fix: api server file formatting (#5054)
* fix: api server file formatting * fix: ruff fixable errors
This commit is contained in:
@@ -63,9 +63,7 @@ class IssueSerializer(BaseSerializer):
|
||||
type_id = serializers.PrimaryKeyRelatedField(
|
||||
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
parent = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Issue.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all(), required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -243,9 +241,7 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
if assignees is not None:
|
||||
# Get the current assignees
|
||||
current_assignees = IssueAssignee.objects.filter(
|
||||
issue=instance
|
||||
).values_list("assignee_id", flat=True)
|
||||
current_assignees = IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True)
|
||||
|
||||
# Get the assignees to add
|
||||
assignees_to_add = list(set(assignees) - set(current_assignees))
|
||||
@@ -254,9 +250,7 @@ class IssueSerializer(BaseSerializer):
|
||||
assignees_to_remove = list(set(current_assignees) - set(assignees))
|
||||
|
||||
# Delete the assignees to remove
|
||||
IssueAssignee.objects.filter(
|
||||
issue=instance, assignee_id__in=assignees_to_remove
|
||||
).delete()
|
||||
IssueAssignee.objects.filter(issue=instance, assignee_id__in=assignees_to_remove).delete()
|
||||
|
||||
try:
|
||||
IssueAssignee.objects.bulk_create(
|
||||
@@ -279,9 +273,7 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
if labels is not None:
|
||||
# Get the current labels
|
||||
current_labels = IssueLabel.objects.filter(issue=instance).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
current_labels = IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True)
|
||||
|
||||
# Get the labels to add
|
||||
labels_to_add = list(set(labels) - set(current_labels))
|
||||
@@ -290,9 +282,7 @@ class IssueSerializer(BaseSerializer):
|
||||
labels_to_remove = list(set(current_labels) - set(labels))
|
||||
|
||||
# Delete the labels to remove
|
||||
IssueLabel.objects.filter(
|
||||
issue=instance, label_id__in=labels_to_remove
|
||||
).delete()
|
||||
IssueLabel.objects.filter(issue=instance, label_id__in=labels_to_remove).delete()
|
||||
|
||||
# Create the labels to add
|
||||
try:
|
||||
@@ -848,18 +838,12 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="related_issue.project_id", read_only=True
|
||||
)
|
||||
sequence_id = serializers.IntegerField(
|
||||
source="related_issue.sequence_id", read_only=True
|
||||
)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
type_id = serializers.UUIDField(source="related_issue.type.id", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
is_epic = serializers.BooleanField(
|
||||
source="related_issue.type.is_epic", read_only=True
|
||||
)
|
||||
is_epic = serializers.BooleanField(source="related_issue.type.is_epic", read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
|
||||
@@ -899,9 +883,7 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="issue.project_id", read_only=True
|
||||
)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
type_id = serializers.UUIDField(source="issue.type.id", read_only=True)
|
||||
|
||||
@@ -25,7 +25,7 @@ from plane.ee.models import WorkitemTemplate, ProjectFeature
|
||||
from plane.payment.flags.flag import FeatureFlag
|
||||
|
||||
|
||||
class ProjectCreateSerializer(BaseSerializer):
|
||||
class ProjectCreateSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for creating projects with workspace validation.
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ router.register(r"invitations", WorkspaceInvitationsViewset, basename="workspace
|
||||
# Wrap the router URLs with the workspace slug path
|
||||
urlpatterns = [
|
||||
path("workspaces/<str:slug>/", include(router.urls)),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path, include
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import TeamspaceViewSet
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ urlpatterns = [
|
||||
name="workspace-features",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -59,9 +59,7 @@ class CustomerAPIEndpoint(BaseAPIView):
|
||||
return (
|
||||
Customer.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.annotate(
|
||||
customer_request_count=CustomerRequest.objects.filter(
|
||||
customer_id=OuterRef("id")
|
||||
)
|
||||
customer_request_count=CustomerRequest.objects.filter(customer_id=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="count"))
|
||||
.values("count")
|
||||
@@ -122,9 +120,7 @@ class CustomerAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = CustomerSerializer(
|
||||
data=request.data, context={"workspace_id": workspace.id}
|
||||
)
|
||||
serializer = CustomerSerializer(data=request.data, context={"workspace_id": workspace.id})
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
@@ -146,9 +142,7 @@ class CustomerDetailAPIEndpoint(BaseAPIView):
|
||||
return (
|
||||
Customer.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.annotate(
|
||||
customer_request_count=CustomerRequest.objects.filter(
|
||||
customer_id=OuterRef("id")
|
||||
)
|
||||
customer_request_count=CustomerRequest.objects.filter(customer_id=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="count"))
|
||||
.values("count")
|
||||
@@ -246,9 +240,7 @@ class CustomerRequestAPIEndpoint(BaseAPIView):
|
||||
|
||||
def get_queryset(self, customer_id):
|
||||
return (
|
||||
CustomerRequest.objects.filter(
|
||||
customer_id=customer_id, workspace__slug=self.kwargs.get("slug")
|
||||
)
|
||||
CustomerRequest.objects.filter(customer_id=customer_id, workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "customer")
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
).distinct()
|
||||
@@ -283,9 +275,7 @@ class CustomerRequestAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=request_queryset,
|
||||
on_results=lambda requests: CustomerRequestSerializer(
|
||||
requests, many=True
|
||||
).data,
|
||||
on_results=lambda requests: CustomerRequestSerializer(requests, many=True).data,
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
@@ -331,9 +321,7 @@ class CustomerRequestDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
def get_queryset(self, customer_id):
|
||||
return (
|
||||
CustomerRequest.objects.filter(
|
||||
customer_id=customer_id, workspace__slug=self.kwargs.get("slug")
|
||||
)
|
||||
CustomerRequest.objects.filter(customer_id=customer_id, workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "customer")
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
).distinct()
|
||||
@@ -385,13 +373,9 @@ class CustomerRequestDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
Partially update an existing customer request.
|
||||
"""
|
||||
customer_request = CustomerRequest.objects.get(
|
||||
pk=pk, customer_id=customer_id, workspace__slug=slug
|
||||
)
|
||||
customer_request = CustomerRequest.objects.get(pk=pk, customer_id=customer_id, workspace__slug=slug)
|
||||
|
||||
serializer = CustomerRequestSerializer(
|
||||
customer_request, data=request.data, partial=True
|
||||
)
|
||||
serializer = CustomerRequestSerializer(customer_request, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@@ -414,9 +398,7 @@ class CustomerRequestDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
Permanently delete a customer request and unlink any associated issues.
|
||||
"""
|
||||
customer_request = CustomerRequest.objects.get(
|
||||
pk=pk, customer_id=customer_id, workspace__slug=slug
|
||||
)
|
||||
customer_request = CustomerRequest.objects.get(pk=pk, customer_id=customer_id, workspace__slug=slug)
|
||||
customer_request.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -484,9 +466,7 @@ class CustomerIssuesAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Filter by specific customer request if provided
|
||||
if customer_request_id:
|
||||
issues = issues.filter(
|
||||
customer_request_issues__customer_request_id=customer_request_id
|
||||
)
|
||||
issues = issues.filter(customer_request_issues__customer_request_id=customer_request_id)
|
||||
|
||||
# Add search functionality
|
||||
if search:
|
||||
@@ -570,9 +550,7 @@ class CustomerIssuesAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Verify customer request exists if provided
|
||||
if customer_request_id:
|
||||
CustomerRequest.objects.get(
|
||||
pk=customer_request_id, customer_id=customer_id, workspace__slug=slug
|
||||
)
|
||||
CustomerRequest.objects.get(pk=customer_request_id, customer_id=customer_id, workspace__slug=slug)
|
||||
|
||||
# Validate that all issues exist before creating links
|
||||
existing_issues = Issue.objects.filter(
|
||||
@@ -729,9 +707,7 @@ class CustomerPropertiesAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=properties_queryset,
|
||||
on_results=lambda properties: CustomerPropertySerializer(
|
||||
properties, many=True
|
||||
).data,
|
||||
on_results=lambda properties: CustomerPropertySerializer(properties, many=True).data,
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
@@ -757,14 +733,9 @@ class CustomerPropertiesAPIEndpoint(BaseAPIView):
|
||||
options = request.data.pop("options", [])
|
||||
|
||||
# Basic validation
|
||||
if (
|
||||
not request.data.get("is_multi")
|
||||
and len(request.data.get("default_value", [])) > 1
|
||||
):
|
||||
if not request.data.get("is_multi") and len(request.data.get("default_value", [])) > 1:
|
||||
return Response(
|
||||
{
|
||||
"error": "Default value must be a single value for non-multi properties"
|
||||
},
|
||||
{"error": "Default value must be a single value for non-multi properties"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -775,9 +746,7 @@ class CustomerPropertiesAPIEndpoint(BaseAPIView):
|
||||
serializer = CustomerPropertySerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
# Check for duplicate name
|
||||
if CustomerProperty.objects.filter(
|
||||
workspace_id=workspace.id, name=request.data["name"]
|
||||
).exists():
|
||||
if CustomerProperty.objects.filter(workspace_id=workspace.id, name=request.data["name"]).exists():
|
||||
return Response(
|
||||
{"error": "Property with this name already exists in workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -796,9 +765,7 @@ class CustomerPropertiesAPIEndpoint(BaseAPIView):
|
||||
property_options = CustomerPropertyOption.objects.filter(
|
||||
property_id=customer_property.id, workspace__slug=slug
|
||||
)
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(
|
||||
property_options, many=True
|
||||
).data
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(property_options, many=True).data
|
||||
|
||||
return Response(response_data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -809,9 +776,9 @@ class CustomerPropertiesAPIEndpoint(BaseAPIView):
|
||||
workspace_id = customer_property.workspace_id
|
||||
customer_property_id = customer_property.id
|
||||
|
||||
last_id = CustomerPropertyOption.objects.filter(
|
||||
property_id=customer_property_id
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
last_id = CustomerPropertyOption.objects.filter(property_id=customer_property_id).aggregate(
|
||||
largest=Max("sort_order")
|
||||
)["largest"]
|
||||
|
||||
sort_order = (last_id + 10000) if last_id else 10000
|
||||
|
||||
@@ -870,12 +837,8 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
# Include options for OPTION type properties
|
||||
response_data = CustomerPropertySerializer(customer_property).data
|
||||
if customer_property.property_type == "OPTION":
|
||||
options = CustomerPropertyOption.objects.filter(
|
||||
property_id=customer_property.id, workspace__slug=slug
|
||||
)
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(
|
||||
options, many=True
|
||||
).data
|
||||
options = CustomerPropertyOption.objects.filter(property_id=customer_property.id, workspace__slug=slug)
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(options, many=True).data
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -913,14 +876,9 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Validation for default values
|
||||
if (
|
||||
not customer_property.is_multi
|
||||
and len(request.data.get("default_value", [])) > 1
|
||||
):
|
||||
if not customer_property.is_multi and len(request.data.get("default_value", [])) > 1:
|
||||
return Response(
|
||||
{
|
||||
"error": "Default value must be a single value for non-multi properties"
|
||||
},
|
||||
{"error": "Default value must be a single value for non-multi properties"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -928,9 +886,7 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
if request.data.get("is_required", customer_property.is_required):
|
||||
request.data["default_value"] = []
|
||||
|
||||
serializer = CustomerPropertySerializer(
|
||||
customer_property, data=request.data, partial=True
|
||||
)
|
||||
serializer = CustomerPropertySerializer(customer_property, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
# Check for duplicate name (if updating)
|
||||
@@ -943,9 +899,7 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Property with this display name already exists in the workspace"
|
||||
},
|
||||
{"error": "Property with this display name already exists in the workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -961,9 +915,7 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
property_options = CustomerPropertyOption.objects.filter(
|
||||
property_id=customer_property.id, workspace__slug=slug
|
||||
)
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(
|
||||
property_options, many=True
|
||||
).data
|
||||
response_data["options"] = CustomerPropertyOptionSerializer(property_options, many=True).data
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -993,9 +945,9 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
workspace_id = customer_property.workspace_id
|
||||
customer_property_id = customer_property.id
|
||||
|
||||
last_id = CustomerPropertyOption.objects.filter(
|
||||
property_id=customer_property_id
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
last_id = CustomerPropertyOption.objects.filter(property_id=customer_property_id).aggregate(
|
||||
largest=Max("sort_order")
|
||||
)["largest"]
|
||||
|
||||
sort_order = (last_id + 10000) if last_id else 10000
|
||||
|
||||
@@ -1023,9 +975,7 @@ class CustomerPropertyDetailAPIEndpoint(BaseAPIView):
|
||||
property_id=customer_property.id,
|
||||
pk=option["id"],
|
||||
)
|
||||
option_serializer = CustomerPropertyOptionSerializer(
|
||||
property_option, data=option, partial=True
|
||||
)
|
||||
option_serializer = CustomerPropertyOptionSerializer(property_option, data=option, partial=True)
|
||||
if option_serializer.is_valid():
|
||||
option_serializer.save()
|
||||
else:
|
||||
@@ -1125,15 +1075,10 @@ class CustomerPropertyValuesAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Annotate and get values
|
||||
property_values = self._query_annotator(queryset).values(
|
||||
"property_id", "values"
|
||||
)
|
||||
property_values = self._query_annotator(queryset).values("property_id", "values")
|
||||
|
||||
# Create response dictionary
|
||||
response = {
|
||||
str(prop_value["property_id"]): prop_value["values"]
|
||||
for prop_value in property_values
|
||||
}
|
||||
response = {str(prop_value["property_id"]): prop_value["values"] for prop_value in property_values}
|
||||
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1185,9 +1130,7 @@ class CustomerPropertyValuesAPIEndpoint(BaseAPIView):
|
||||
customer_id=customer_id,
|
||||
)
|
||||
|
||||
existing_prop_values = self._query_annotator(existing_prop_queryset).values(
|
||||
"property_id", "values"
|
||||
)
|
||||
existing_prop_values = self._query_annotator(existing_prop_queryset).values("property_id", "values")
|
||||
|
||||
# Get customer properties
|
||||
customer_properties = CustomerProperty.objects.filter(
|
||||
@@ -1212,14 +1155,10 @@ class CustomerPropertyValuesAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Delete old values
|
||||
existing_prop_queryset.filter(
|
||||
property_id__in=customer_property_ids
|
||||
).delete()
|
||||
existing_prop_queryset.filter(property_id__in=customer_property_ids).delete()
|
||||
|
||||
# Bulk create new values
|
||||
CustomerPropertyValue.objects.bulk_create(
|
||||
bulk_customer_property_values, batch_size=10
|
||||
)
|
||||
CustomerPropertyValue.objects.bulk_create(bulk_customer_property_values, batch_size=10)
|
||||
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -1326,15 +1265,10 @@ class CustomerPropertyValueDetailAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Annotate and get values
|
||||
property_values = self._query_annotator(queryset).values(
|
||||
"property_id", "values"
|
||||
)
|
||||
property_values = self._query_annotator(queryset).values("property_id", "values")
|
||||
|
||||
# Create response dictionary
|
||||
response = {
|
||||
str(prop_value["property_id"]): prop_value["values"]
|
||||
for prop_value in property_values
|
||||
}
|
||||
response = {str(prop_value["property_id"]): prop_value["values"] for prop_value in property_values}
|
||||
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1372,9 +1306,7 @@ class CustomerPropertyValueDetailAPIEndpoint(BaseAPIView):
|
||||
try:
|
||||
# Verify customer and property exist
|
||||
Customer.objects.get(pk=customer_id, workspace__slug=slug)
|
||||
customer_property = CustomerProperty.objects.get(
|
||||
workspace__slug=slug, pk=property_id
|
||||
)
|
||||
customer_property = CustomerProperty.objects.get(workspace__slug=slug, pk=property_id)
|
||||
|
||||
existing_prop_queryset = CustomerPropertyValue.objects.filter(
|
||||
workspace__slug=slug, customer_id=customer_id, property_id=property_id
|
||||
@@ -1383,13 +1315,9 @@ class CustomerPropertyValueDetailAPIEndpoint(BaseAPIView):
|
||||
values = request.data.get("values", [])
|
||||
|
||||
# Check if property is required
|
||||
if customer_property.is_required and (
|
||||
not values or not [v for v in values if v]
|
||||
):
|
||||
if customer_property.is_required and (not values or not [v for v in values if v]):
|
||||
return Response(
|
||||
{
|
||||
"error": f"{customer_property.display_name} is a required property"
|
||||
},
|
||||
{"error": f"{customer_property.display_name} is a required property"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -1414,9 +1342,7 @@ class CustomerPropertyValueDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Delete old values and create new ones
|
||||
existing_prop_queryset.delete()
|
||||
CustomerPropertyValue.objects.bulk_create(
|
||||
property_values, batch_size=10
|
||||
)
|
||||
CustomerPropertyValue.objects.bulk_create(property_values, batch_size=10)
|
||||
else:
|
||||
raise ValidationError("Invalid property type")
|
||||
|
||||
|
||||
@@ -199,9 +199,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
many=True,
|
||||
@@ -258,9 +256,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
|
||||
)
|
||||
queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
@@ -306,17 +302,10 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
Create a new development cycle with specified name, description, and date range.
|
||||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
|
||||
serializer = CycleCreateSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
serializer = CycleCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@@ -359,9 +348,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -509,9 +496,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
@@ -524,20 +509,14 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleUpdateSerializer(
|
||||
cycle, data=request.data, partial=True, context={"request": request}
|
||||
)
|
||||
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@@ -545,9 +524,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", cycle.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", cycle.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@@ -604,11 +581,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -628,9 +601,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle", entity_identifier=pk, project_id=project_id
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -768,9 +739,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -789,9 +758,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Move a completed cycle to archived status for historical tracking.
|
||||
Only cycles that have ended can be archived.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
if cycle.end_date >= timezone.now():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
@@ -822,9 +789,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Restore an archived cycle to active status, making it available for regular use.
|
||||
The cycle will reappear in active cycle lists.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
cycle.archived_at = None
|
||||
cycle.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -887,9 +852,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
# List
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
|
||||
)
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -926,9 +889,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -962,9 +923,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
@@ -976,13 +935,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Get all CycleWorkItems already created
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
|
||||
existing_issues = [
|
||||
str(cycle_issue.issue_id)
|
||||
for cycle_issue in cycle_issues
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
@@ -1072,9 +1027,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", created_records
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", created_records),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -1150,9 +1103,7 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
cycle_id=cycle_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
serializer = CycleIssueSerializer(
|
||||
cycle_issue, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@cycle_docs(
|
||||
@@ -1269,7 +1220,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
{"error": "New Cycle Id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
old_cycle = Cycle.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -1291,7 +1242,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
user_id=self.request.user.id,
|
||||
)
|
||||
|
||||
|
||||
# Handle the result
|
||||
if result.get("success"):
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -99,7 +99,7 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class ProjectMemberSiloEndpoint(BaseAPIView):
|
||||
#TODO: Remove this endpoint once the silo is updated to use the new endpoint
|
||||
# TODO: Remove this endpoint once the silo is updated to use the new endpoint
|
||||
|
||||
permission_classes = [ProjectMemberPermission]
|
||||
use_read_replica = True
|
||||
@@ -150,10 +150,7 @@ class ProjectMemberSiloEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
# ------------------- Validation -------------------
|
||||
if (
|
||||
request.data.get("email") is None
|
||||
or request.data.get("display_name") is None
|
||||
):
|
||||
if request.data.get("email") is None or request.data.get("display_name") is None:
|
||||
return Response(
|
||||
{
|
||||
"error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing."
|
||||
@@ -166,9 +163,7 @@ class ProjectMemberSiloEndpoint(BaseAPIView):
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
workspace = Workspace.objects.filter(slug=slug).first()
|
||||
project = Project.objects.filter(pk=project_id).first()
|
||||
@@ -186,14 +181,10 @@ class ProjectMemberSiloEndpoint(BaseAPIView):
|
||||
|
||||
if user:
|
||||
# Check if user is part of the workspace
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace=workspace, member=user
|
||||
).first()
|
||||
workspace_member = WorkspaceMember.objects.filter(workspace=workspace, member=user).first()
|
||||
if workspace_member:
|
||||
# Check if user is part of the project
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project=project, member=user
|
||||
).first()
|
||||
project_member = ProjectMember.objects.filter(project=project, member=user).first()
|
||||
if project_member:
|
||||
return Response(
|
||||
{"error": "User is already part of the workspace and project"},
|
||||
@@ -238,7 +229,6 @@ class ProjectMemberSiloEndpoint(BaseAPIView):
|
||||
return Response(user_data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
|
||||
class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectMemberPermission]
|
||||
use_read_replica = True
|
||||
@@ -305,7 +295,6 @@ class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# API endpoint to get and update a project member
|
||||
class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
|
||||
|
||||
@extend_schema(
|
||||
operation_id="get_project_member",
|
||||
summary="Get project member",
|
||||
|
||||
@@ -22,7 +22,7 @@ from plane.utils.openapi import (
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
TEAMSPACE_ID_PARAMETER,
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER
|
||||
PER_PAGE_PARAMETER,
|
||||
)
|
||||
|
||||
|
||||
@@ -160,15 +160,20 @@ class TeamspaceViewSet(BaseViewSet):
|
||||
operation_id="add_teamspace_projects",
|
||||
summary="Add projects to a teamspace",
|
||||
description="Add projects to a teamspace",
|
||||
request=OpenApiRequest(request={"type": "object", "properties": {
|
||||
"project_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
request=OpenApiRequest(
|
||||
request={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
}
|
||||
),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
TEAMSPACE_ID_PARAMETER,
|
||||
@@ -203,15 +208,20 @@ class TeamspaceViewSet(BaseViewSet):
|
||||
operation_id="remove_teamspace_projects",
|
||||
summary="Remove projects from a teamspace",
|
||||
description="Remove projects from a teamspace by its ID",
|
||||
request=OpenApiRequest(request={"type": "object", "properties": {
|
||||
"project_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
request=OpenApiRequest(
|
||||
request={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
}
|
||||
),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
TEAMSPACE_ID_PARAMETER,
|
||||
@@ -256,15 +266,20 @@ class TeamspaceViewSet(BaseViewSet):
|
||||
operation_id="add_teamspace_members",
|
||||
summary="Add members to a teamspace",
|
||||
description="Add members to a teamspace",
|
||||
request=OpenApiRequest(request={"type": "object", "properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
request=OpenApiRequest(
|
||||
request={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
}
|
||||
),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
TEAMSPACE_ID_PARAMETER,
|
||||
@@ -297,15 +312,20 @@ class TeamspaceViewSet(BaseViewSet):
|
||||
operation_id="remove_teamspace_members",
|
||||
summary="Delete members from a teamspace",
|
||||
description="Delete members from a teamspace by its ID",
|
||||
request=OpenApiRequest(request={"type": "object", "properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
request=OpenApiRequest(
|
||||
request={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"member_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
}
|
||||
),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
TEAMSPACE_ID_PARAMETER,
|
||||
|
||||
@@ -10,6 +10,7 @@ from plane.db.models import User
|
||||
from plane.utils.openapi.decorators import user_docs
|
||||
from plane.utils.openapi import USER_EXAMPLE
|
||||
|
||||
|
||||
class UserEndpoint(BaseAPIView):
|
||||
serializer_class = UserLiteSerializer
|
||||
model = User
|
||||
|
||||
@@ -37,9 +37,7 @@ def check_teamspace_membership(view, request: Request) -> bool:
|
||||
).values_list("team_space_id", flat=True)
|
||||
|
||||
# return True if the user is a member of any of the teamspace
|
||||
return TeamspaceMember.objects.filter(
|
||||
member=request.user, team_space_id__in=teamspace_ids
|
||||
).exists()
|
||||
return TeamspaceMember.objects.filter(member=request.user, team_space_id__in=teamspace_ids).exists()
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -148,14 +148,13 @@ from .deploy_board import DeployBoardSerializer
|
||||
|
||||
|
||||
# Extended serializers
|
||||
from .extended.issue import ExtendedIssueCreateSerializer as IssueCreateSerializer # noqa: F811
|
||||
from .extended.issue import ExtendedIssueCreateSerializer as IssueCreateSerializer # noqa: F811
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Base serializers
|
||||
"BaseSerializer",
|
||||
"DynamicBaseSerializer",
|
||||
|
||||
# User serializers
|
||||
"UserSerializer",
|
||||
"UserLiteSerializer",
|
||||
@@ -166,7 +165,6 @@ __all__ = [
|
||||
"UserMeSettingsSerializer",
|
||||
"ProfileSerializer",
|
||||
"AccountSerializer",
|
||||
|
||||
# Workspace serializers
|
||||
"WorkSpaceSerializer",
|
||||
"WorkSpaceMemberSerializer",
|
||||
@@ -181,7 +179,6 @@ __all__ = [
|
||||
"WorkspaceHomePreferenceSerializer",
|
||||
"StickySerializer",
|
||||
"WorkspaceUserMeSerializer",
|
||||
|
||||
# Project serializers
|
||||
"ProjectSerializer",
|
||||
"ProjectListSerializer",
|
||||
@@ -194,25 +191,20 @@ __all__ = [
|
||||
"ProjectMemberAdminSerializer",
|
||||
"ProjectPublicMemberSerializer",
|
||||
"ProjectMemberRoleSerializer",
|
||||
|
||||
# State serializers
|
||||
"StateSerializer",
|
||||
"StateLiteSerializer",
|
||||
|
||||
# View serializers
|
||||
"IssueViewSerializer",
|
||||
"ViewIssueListSerializer",
|
||||
|
||||
# Cycle serializers
|
||||
"CycleSerializer",
|
||||
"CycleIssueSerializer",
|
||||
"CycleWriteSerializer",
|
||||
"CycleUserPropertiesSerializer",
|
||||
"EntityProgressSerializer",
|
||||
|
||||
# Asset serializers
|
||||
"FileAssetSerializer",
|
||||
|
||||
# Issue serializers
|
||||
"IssueCreateSerializer",
|
||||
"IssueActivitySerializer",
|
||||
@@ -242,7 +234,6 @@ __all__ = [
|
||||
"IssueListDetailSerializer",
|
||||
"IssueDuplicateSerializer",
|
||||
"IssueAttachmentSerializer",
|
||||
|
||||
# Module serializers
|
||||
"ModuleDetailSerializer",
|
||||
"ModuleWriteSerializer",
|
||||
@@ -250,14 +241,11 @@ __all__ = [
|
||||
"ModuleIssueSerializer",
|
||||
"ModuleLinkSerializer",
|
||||
"ModuleUserPropertiesSerializer",
|
||||
|
||||
# API serializers
|
||||
"APITokenSerializer",
|
||||
"APITokenReadSerializer",
|
||||
|
||||
# Importer serializers
|
||||
"ImporterSerializer",
|
||||
|
||||
# Page serializers
|
||||
"PageSerializer",
|
||||
"PageLiteSerializer",
|
||||
@@ -266,42 +254,33 @@ __all__ = [
|
||||
"PageBinaryUpdateSerializer",
|
||||
"PageVersionDetailSerializer",
|
||||
"PageUserSerializer",
|
||||
|
||||
# Estimate serializers
|
||||
"EstimateSerializer",
|
||||
"EstimatePointSerializer",
|
||||
"EstimateReadSerializer",
|
||||
"WorkspaceEstimateSerializer",
|
||||
|
||||
# Intake serializers
|
||||
"IntakeSerializer",
|
||||
"IntakeIssueSerializer",
|
||||
"IssueStateIntakeSerializer",
|
||||
"IntakeIssueLiteSerializer",
|
||||
"IntakeIssueDetailSerializer",
|
||||
|
||||
# Analytic serializers
|
||||
"AnalyticViewSerializer",
|
||||
|
||||
# Notification serializers
|
||||
"NotificationSerializer",
|
||||
"UserNotificationPreferenceSerializer",
|
||||
|
||||
# Exporter serializers
|
||||
"ExporterHistorySerializer",
|
||||
|
||||
# Webhook serializers
|
||||
"WebhookSerializer",
|
||||
"WebhookLogSerializer",
|
||||
|
||||
# Favorite serializers
|
||||
"UserFavoriteSerializer",
|
||||
|
||||
# Draft serializers
|
||||
"DraftIssueCreateSerializer",
|
||||
"DraftIssueSerializer",
|
||||
"DraftIssueDetailSerializer",
|
||||
|
||||
# Integration serializers
|
||||
"IntegrationSerializer",
|
||||
"WorkspaceIntegrationSerializer",
|
||||
@@ -310,7 +289,6 @@ __all__ = [
|
||||
"GithubRepositorySyncSerializer",
|
||||
"GithubCommentSyncSerializer",
|
||||
"SlackProjectSyncSerializer",
|
||||
|
||||
# Deploy board serializers
|
||||
"DeployBoardSerializer",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -70,7 +70,8 @@ class ExtendedIssueCreateSerializer(IssueCreateSerializer):
|
||||
)
|
||||
):
|
||||
intake_responsibilities = IntakeResponsibility.objects.filter(
|
||||
project_id=project_id, intake=intake_id,
|
||||
project_id=project_id,
|
||||
intake=intake_id,
|
||||
type=IntakeResponsibilityTypeChoices.ASSIGNEE,
|
||||
).values_list("user_id", flat=True)
|
||||
try:
|
||||
|
||||
@@ -177,6 +177,8 @@ class PageLiteSerializer(BaseSerializer):
|
||||
"is_shared",
|
||||
"sort_order",
|
||||
]
|
||||
|
||||
|
||||
class PageVersionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = PageVersion
|
||||
|
||||
@@ -17,12 +17,8 @@ class ViewIssueListSerializer(serializers.Serializer):
|
||||
|
||||
# Check if the user has access to the customer request count
|
||||
if slug and user_id:
|
||||
if check_workspace_feature_flag(
|
||||
feature_key=FeatureFlag.CUSTOMERS, slug=slug, user_id=user_id
|
||||
):
|
||||
self.fields["customer_request_ids"] = serializers.ListField(
|
||||
read_only=True
|
||||
)
|
||||
if check_workspace_feature_flag(feature_key=FeatureFlag.CUSTOMERS, slug=slug, user_id=user_id):
|
||||
self.fields["customer_request_ids"] = serializers.ListField(read_only=True)
|
||||
self.fields["customer_ids"] = serializers.ListField(read_only=True)
|
||||
|
||||
def get_assignee_ids(self, instance):
|
||||
@@ -35,15 +31,10 @@ class ViewIssueListSerializer(serializers.Serializer):
|
||||
return [module.module_id for module in instance.issue_module.all()]
|
||||
|
||||
def get_customer_ids(self, instance):
|
||||
return [
|
||||
customer.customer_id for customer in instance.customer_request_issues.all()
|
||||
]
|
||||
return [customer.customer_id for customer in instance.customer_request_issues.all()]
|
||||
|
||||
def get_customer_request_ids(self, instance):
|
||||
return [
|
||||
customer_request.customer_request_id
|
||||
for customer_request in instance.customer_request_issues.all()
|
||||
]
|
||||
return [customer_request.customer_request_id for customer_request in instance.customer_request_issues.all()]
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = {
|
||||
|
||||
@@ -8,4 +8,4 @@ urlpatterns = [
|
||||
ExportIssuesEndpoint.as_view(),
|
||||
name="export-issues",
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -21,9 +21,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"integrations/<uuid:pk>/",
|
||||
IntegrationViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
IntegrationViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="integrations",
|
||||
),
|
||||
path(
|
||||
|
||||
@@ -3,19 +3,7 @@ from rest_framework import status
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
QuerySet,
|
||||
Q,
|
||||
Count,
|
||||
Case,
|
||||
When,
|
||||
Value,
|
||||
F,
|
||||
CharField,
|
||||
Max,
|
||||
OuterRef,
|
||||
Subquery
|
||||
)
|
||||
from django.db.models import QuerySet, Q, Count, Case, When, Value, F, CharField, Max, OuterRef, Subquery
|
||||
from django.http import HttpRequest
|
||||
from django.db.models.functions import TruncMonth, Cast, Concat, Coalesce
|
||||
from django.utils import timezone
|
||||
@@ -34,7 +22,7 @@ from plane.db.models import (
|
||||
Workspace,
|
||||
ProjectMember,
|
||||
User,
|
||||
Page
|
||||
Page,
|
||||
)
|
||||
from plane.ee.models import EntityUpdates, ProjectAttribute
|
||||
from plane.utils.build_chart import build_analytics_chart
|
||||
@@ -395,34 +383,18 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView):
|
||||
projects_qs = (
|
||||
Project.objects.filter(id__in=project_ids)
|
||||
.annotate(
|
||||
total_work_items=Coalesce(
|
||||
Subquery(issue_subquery.values("total_work_items")[:1]), Value(0)
|
||||
),
|
||||
total_work_items=Coalesce(Subquery(issue_subquery.values("total_work_items")[:1]), Value(0)),
|
||||
completed_work_items=Coalesce(
|
||||
Subquery(issue_subquery.values("completed_work_items")[:1]),
|
||||
Value(0),
|
||||
),
|
||||
total_epics=Coalesce(
|
||||
Subquery(issue_subquery.values("total_epics")[:1]), Value(0)
|
||||
),
|
||||
total_intake=Coalesce(
|
||||
Subquery(issue_subquery.values("total_intake")[:1]), Value(0)
|
||||
),
|
||||
total_cycles=Coalesce(
|
||||
Subquery(cycle_subquery.values("total_cycles")[:1]), Value(0)
|
||||
),
|
||||
total_modules=Coalesce(
|
||||
Subquery(module_subquery.values("total_modules")[:1]), Value(0)
|
||||
),
|
||||
total_members=Coalesce(
|
||||
Subquery(member_subquery.values("total_members")[:1]), Value(0)
|
||||
),
|
||||
total_pages=Coalesce(
|
||||
Subquery(page_subquery.values("total_pages")[:1]), Value(0)
|
||||
),
|
||||
total_views=Coalesce(
|
||||
Subquery(view_subquery.values("total_views")[:1]), Value(0)
|
||||
),
|
||||
total_epics=Coalesce(Subquery(issue_subquery.values("total_epics")[:1]), Value(0)),
|
||||
total_intake=Coalesce(Subquery(issue_subquery.values("total_intake")[:1]), Value(0)),
|
||||
total_cycles=Coalesce(Subquery(cycle_subquery.values("total_cycles")[:1]), Value(0)),
|
||||
total_modules=Coalesce(Subquery(module_subquery.values("total_modules")[:1]), Value(0)),
|
||||
total_members=Coalesce(Subquery(member_subquery.values("total_members")[:1]), Value(0)),
|
||||
total_pages=Coalesce(Subquery(page_subquery.values("total_pages")[:1]), Value(0)),
|
||||
total_views=Coalesce(Subquery(view_subquery.values("total_views")[:1]), Value(0)),
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
|
||||
@@ -34,9 +34,7 @@ class ProxyUploadEndpoint(BaseAPIView):
|
||||
# Decode the original S3 URL (we don't actually use it, just for validation)
|
||||
_ = base64.urlsafe_b64decode(encoded_s3_url.encode()).decode()
|
||||
except Exception:
|
||||
return Response(
|
||||
{"error": "Invalid S3 URL"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid S3 URL"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get the uploaded file and form data
|
||||
try:
|
||||
@@ -48,9 +46,7 @@ class ProxyUploadEndpoint(BaseAPIView):
|
||||
|
||||
# Validate required fields
|
||||
if not all([object_key, content_type, policy]):
|
||||
return Response(
|
||||
{"error": "Invalid request"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid request"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# Upload directly to S3 using boto3
|
||||
storage = S3Storage(is_server=True)
|
||||
|
||||
@@ -120,9 +116,7 @@ class ProxyDownloadEndpoint(BaseAPIView):
|
||||
# Get object metadata first
|
||||
metadata = storage.get_object_metadata(object_name)
|
||||
if not metadata:
|
||||
return Response(
|
||||
{"error": "File not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "File not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Stream the file from S3
|
||||
def stream_from_s3():
|
||||
@@ -150,9 +144,7 @@ class ProxyDownloadEndpoint(BaseAPIView):
|
||||
disposition = params.get("disposition", "inline")
|
||||
filename = params.get("filename")
|
||||
if filename:
|
||||
response["Content-Disposition"] = (
|
||||
f'{disposition}; filename="{filename}"'
|
||||
)
|
||||
response["Content-Disposition"] = f'{disposition}; filename="{filename}"'
|
||||
else:
|
||||
response["Content-Disposition"] = disposition
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from plane.utils.core.mixins import ReadReplicaControlMixin
|
||||
|
||||
logger = logging.getLogger("plane.api")
|
||||
|
||||
|
||||
class TimezoneMixin:
|
||||
"""
|
||||
This enables timezone conversion according
|
||||
@@ -88,8 +89,8 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
extra={
|
||||
"error_code": "VALIDATION_ERROR",
|
||||
"error_message": str(e),
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please provide valid detail"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -101,7 +102,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
extra={
|
||||
"error_code": "OBJECT_DOES_NOT_EXIST",
|
||||
"error_message": str(e),
|
||||
}
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required object does not exist."},
|
||||
@@ -114,7 +115,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
extra={
|
||||
"error_code": "KEY_ERROR",
|
||||
"error_message": str(e),
|
||||
}
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required key does not exist."},
|
||||
@@ -205,8 +206,8 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
|
||||
extra={
|
||||
"error_code": "VALIDATION_ERROR",
|
||||
"error_message": str(e),
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please provide valid detail"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -218,7 +219,7 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
|
||||
extra={
|
||||
"error_code": "OBJECT_DOES_NOT_EXIST",
|
||||
"error_message": str(e),
|
||||
}
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required object does not exist."},
|
||||
@@ -231,7 +232,7 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
|
||||
extra={
|
||||
"error_code": "KEY_ERROR",
|
||||
"error_message": str(e),
|
||||
}
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required key does not exist."},
|
||||
|
||||
@@ -63,9 +63,6 @@ from plane.utils.timezone_converter import (
|
||||
convert_to_utc,
|
||||
user_timezone_converter,
|
||||
)
|
||||
from plane.ee.bgtasks.entity_issue_state_progress_task import (
|
||||
entity_issue_state_activity_task,
|
||||
)
|
||||
from plane.ee.models import EntityProgress
|
||||
|
||||
|
||||
@@ -105,9 +102,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only(
|
||||
"avatar_asset", "first_name", "id"
|
||||
).distinct(),
|
||||
queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@@ -158,8 +153,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=current_time_in_utc)
|
||||
& Q(end_date__gte=current_time_in_utc),
|
||||
Q(start_date__lte=current_time_in_utc) & Q(end_date__gte=current_time_in_utc),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
|
||||
@@ -178,11 +172,7 @@ class CycleViewSet(BaseViewSet):
|
||||
"issue_cycle__issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
|
||||
& (
|
||||
Q(
|
||||
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
& (Q(issue_cycle__issue__issue_assignee__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
@@ -214,9 +204,7 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc)
|
||||
|
||||
data = queryset.values(
|
||||
# necessary fields
|
||||
@@ -283,16 +271,10 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleWriteSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = CycleWriteSerializer(data=request.data, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, owned_by=request.user, version=2)
|
||||
cycle = (
|
||||
@@ -332,9 +314,7 @@ class CycleViewSet(BaseViewSet):
|
||||
project_timezone = project.timezone
|
||||
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
cycle = user_timezone_converter(
|
||||
cycle, datetime_fields, project_timezone
|
||||
)
|
||||
cycle = user_timezone_converter(cycle, datetime_fields, project_timezone)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
@@ -350,17 +330,13 @@ class CycleViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
queryset = self.get_queryset().filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
queryset = self.get_queryset().filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
cycle = queryset.first()
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
@@ -368,23 +344,17 @@ class CycleViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order for a completed cycle``
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -617,9 +587,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if data is None:
|
||||
return Response(
|
||||
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
queryset = queryset.first()
|
||||
# Fetch the project timezone
|
||||
@@ -641,11 +609,7 @@ class CycleViewSet(BaseViewSet):
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -696,9 +660,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
start_date = convert_to_utc(
|
||||
date=str(start_date), project_id=project_id, is_start_date=True
|
||||
)
|
||||
start_date = convert_to_utc(date=str(start_date), project_id=project_id, is_start_date=True)
|
||||
end_date = convert_to_utc(
|
||||
date=str(end_date),
|
||||
project_id=project_id,
|
||||
@@ -804,12 +766,8 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
|
||||
cycle_properties.rich_filters = request.data.get(
|
||||
"rich_filters", cycle_properties.rich_filters
|
||||
)
|
||||
cycle_properties.display_filters = request.data.get(
|
||||
"display_filters", cycle_properties.display_filters
|
||||
)
|
||||
cycle_properties.rich_filters = request.data.get("rich_filters", cycle_properties.rich_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
|
||||
)
|
||||
@@ -833,13 +791,9 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
class CycleProgressEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, id=cycle_id
|
||||
).first()
|
||||
cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id).first()
|
||||
if not cycle:
|
||||
return Response(
|
||||
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
aggregate_estimates = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
@@ -885,9 +839,7 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
output_field=FloatField(),
|
||||
)
|
||||
),
|
||||
total_estimate_points=Sum(
|
||||
"value_as_float", default=Value(0), output_field=FloatField()
|
||||
),
|
||||
total_estimate_points=Sum("value_as_float", default=Value(0), output_field=FloatField()),
|
||||
)
|
||||
)
|
||||
if cycle.progress_snapshot:
|
||||
@@ -947,22 +899,11 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
return Response(
|
||||
{
|
||||
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"]
|
||||
or 0,
|
||||
"unstarted_estimate_points": aggregate_estimates[
|
||||
"unstarted_estimate_point"
|
||||
]
|
||||
or 0,
|
||||
"started_estimate_points": aggregate_estimates["started_estimate_point"]
|
||||
or 0,
|
||||
"cancelled_estimate_points": aggregate_estimates[
|
||||
"cancelled_estimate_point"
|
||||
]
|
||||
or 0,
|
||||
"completed_estimate_points": aggregate_estimates[
|
||||
"completed_estimate_points"
|
||||
]
|
||||
or 0,
|
||||
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] or 0,
|
||||
"unstarted_estimate_points": aggregate_estimates["unstarted_estimate_point"] or 0,
|
||||
"started_estimate_points": aggregate_estimates["started_estimate_point"] or 0,
|
||||
"cancelled_estimate_points": aggregate_estimates["cancelled_estimate_point"] or 0,
|
||||
"completed_estimate_points": aggregate_estimates["completed_estimate_points"] or 0,
|
||||
"total_estimate_points": aggregate_estimates["total_estimate_points"],
|
||||
"backlog_issues": backlog_issues,
|
||||
"total_issues": total_issues,
|
||||
@@ -980,9 +921,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
analytic_type = request.GET.get("type", "issues")
|
||||
cycle = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, id=cycle_id
|
||||
)
|
||||
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle__issue__id",
|
||||
@@ -1068,9 +1007,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@@ -1105,9 +1042,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@@ -1209,11 +1144,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
.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(total_issues=Count("label_id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
|
||||
@@ -239,18 +239,14 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
existing_issues = [str(cycle_issue.issue_id) for cycle_issue in cycle_issues]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
issue_cycle_data_added = [
|
||||
{"issue_id": str(issue_id), "cycle_id": str(cycle_id)}
|
||||
for issue_id in issues
|
||||
]
|
||||
issue_cycle_data_added = [{"issue_id": str(issue_id), "cycle_id": str(cycle_id)} for issue_id in issues]
|
||||
|
||||
issues_removed = CycleIssue.objects.filter(
|
||||
issue_id__in=existing_issues, workspace__slug=slug
|
||||
).values("issue_id", "cycle_id")
|
||||
issues_removed = CycleIssue.objects.filter(issue_id__in=existing_issues, workspace__slug=slug).values(
|
||||
"issue_id", "cycle_id"
|
||||
)
|
||||
|
||||
issue_cycle_data_removed = [
|
||||
{"issue_id": str(issue["issue_id"]), "cycle_id": str(issue["cycle_id"])}
|
||||
for issue in issues_removed
|
||||
{"issue_id": str(issue["issue_id"]), "cycle_id": str(issue["cycle_id"])} for issue in issues_removed
|
||||
]
|
||||
|
||||
# New issues to create
|
||||
|
||||
@@ -2,4 +2,4 @@ from .base import ExportIssuesEndpoint
|
||||
|
||||
__all__ = [
|
||||
"ExportIssuesEndpoint",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -57,9 +57,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
integration__provider="github", workspace__slug=slug
|
||||
)
|
||||
|
||||
access_tokens_url = workspace_integration.metadata.get(
|
||||
"access_tokens_url", False
|
||||
)
|
||||
access_tokens_url = workspace_integration.metadata.get("access_tokens_url", False)
|
||||
|
||||
if not access_tokens_url:
|
||||
return Response(
|
||||
@@ -69,9 +67,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issue_count, labels, collaborators = get_github_repo_details(
|
||||
access_tokens_url, owner, repo
|
||||
)
|
||||
issue_count, labels, collaborators = get_github_repo_details(access_tokens_url, owner, repo)
|
||||
return Response(
|
||||
{
|
||||
"issue_count": issue_count,
|
||||
@@ -92,25 +88,19 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
|
||||
for key, error_message in params.items():
|
||||
if not request.GET.get(key, False):
|
||||
return Response(
|
||||
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": error_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
project_key = request.GET.get("project_key", "")
|
||||
api_token = request.GET.get("api_token", "")
|
||||
email = request.GET.get("email", "")
|
||||
cloud_hostname = request.GET.get("cloud_hostname", "")
|
||||
|
||||
response = jira_project_issue_summary(
|
||||
email, api_token, project_key, cloud_hostname
|
||||
)
|
||||
response = jira_project_issue_summary(email, api_token, project_key, cloud_hostname)
|
||||
if "error" in response:
|
||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
{"error": "Service not supported yet"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Service not supported yet"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ImportServiceEndpoint(BaseAPIView):
|
||||
@@ -120,9 +110,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
project_id = request.data.get("project_id", False)
|
||||
|
||||
if not project_id:
|
||||
return Response(
|
||||
{"error": "Project ID is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Project ID is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
@@ -136,13 +124,9 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
api_token = APIToken.objects.filter(
|
||||
user=request.user, workspace=workspace
|
||||
).first()
|
||||
api_token = APIToken.objects.filter(user=request.user, workspace=workspace).first()
|
||||
if api_token is None:
|
||||
api_token = APIToken.objects.create(
|
||||
user=request.user, label="Importer", workspace=workspace
|
||||
)
|
||||
api_token = APIToken.objects.create(user=request.user, label="Importer", workspace=workspace)
|
||||
|
||||
importer = Importer.objects.create(
|
||||
service=service,
|
||||
@@ -185,13 +169,9 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
{"error": "Data, config and metadata are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
api_token = APIToken.objects.filter(
|
||||
user=request.user, workspace=workspace
|
||||
).first()
|
||||
api_token = APIToken.objects.filter(user=request.user, workspace=workspace).first()
|
||||
if api_token is None:
|
||||
api_token = APIToken.objects.create(
|
||||
user=request.user, label="Importer", workspace=workspace
|
||||
)
|
||||
api_token = APIToken.objects.create(user=request.user, label="Importer", workspace=workspace)
|
||||
|
||||
importer = Importer.objects.create(
|
||||
service=service,
|
||||
@@ -210,9 +190,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
serializer = ImporterSerializer(importer)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(
|
||||
{"error": "Servivce not supported yet"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Servivce not supported yet"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request, slug):
|
||||
imports = (
|
||||
@@ -252,9 +230,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateServiceImportStatusEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id, service, importer_id):
|
||||
importer = Importer.objects.get(
|
||||
pk=importer_id, workspace__slug=slug, project_id=project_id, service=service
|
||||
)
|
||||
importer = Importer.objects.get(pk=importer_id, workspace__slug=slug, project_id=project_id, service=service)
|
||||
importer.status = request.data.get("status", "processing")
|
||||
importer.save()
|
||||
return Response(status.HTTP_200_OK)
|
||||
@@ -266,38 +242,28 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
# Get the default state
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), project_id=project_id, default=True
|
||||
).first()
|
||||
default_state = State.objects.filter(~Q(name="Triage"), project_id=project_id, default=True).first()
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), project_id=project_id
|
||||
).first()
|
||||
default_state = State.objects.filter(~Q(name="Triage"), project_id=project_id).first()
|
||||
|
||||
# Get the maximum sequence_id
|
||||
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(largest=Max("sequence"))["largest"]
|
||||
|
||||
last_id = 1 if last_id is None else last_id + 1
|
||||
|
||||
# Get the maximum sort order
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project_id=project_id, state=default_state
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
largest_sort_order = Issue.objects.filter(project_id=project_id, state=default_state).aggregate(
|
||||
largest=Max("sort_order")
|
||||
)["largest"]
|
||||
|
||||
largest_sort_order = (
|
||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
)
|
||||
largest_sort_order = 65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
|
||||
# Get the issues_data
|
||||
issues_data = request.data.get("issues_data", [])
|
||||
|
||||
if not len(issues_data):
|
||||
return Response(
|
||||
{"error": "Issue data is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issue data is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Issues
|
||||
bulk_issues = []
|
||||
@@ -306,19 +272,12 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
Issue(
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
state_id=(
|
||||
issue_data.get("state")
|
||||
if issue_data.get("state", False)
|
||||
else default_state.id
|
||||
),
|
||||
state_id=(issue_data.get("state") if issue_data.get("state", False) else default_state.id),
|
||||
name=issue_data.get("name", "Issue Created through Bulk"),
|
||||
description_html=issue_data.get("description_html", "<p></p>"),
|
||||
description_stripped=(
|
||||
None
|
||||
if (
|
||||
issue_data.get("description_html") == ""
|
||||
or issue_data.get("description_html") is None
|
||||
)
|
||||
if (issue_data.get("description_html") == "" or issue_data.get("description_html") is None)
|
||||
else strip_tags(issue_data.get("description_html"))
|
||||
),
|
||||
sequence_id=last_id,
|
||||
@@ -333,9 +292,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
largest_sort_order = largest_sort_order + 10000
|
||||
last_id = last_id + 1
|
||||
|
||||
issues = Issue.objects.bulk_create(
|
||||
bulk_issues, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
issues = Issue.objects.bulk_create(bulk_issues, batch_size=100, ignore_conflicts=True)
|
||||
|
||||
# Sequences
|
||||
_ = IssueSequence.objects.bulk_create(
|
||||
@@ -366,9 +323,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
for label_id in labels_list
|
||||
]
|
||||
|
||||
_ = IssueLabel.objects.bulk_create(
|
||||
bulk_issue_labels, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
_ = IssueLabel.objects.bulk_create(bulk_issue_labels, batch_size=100, ignore_conflicts=True)
|
||||
|
||||
# Attach Assignees
|
||||
bulk_issue_assignees = []
|
||||
@@ -385,9 +340,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
for assignee_id in assignees_list
|
||||
]
|
||||
|
||||
_ = IssueAssignee.objects.bulk_create(
|
||||
bulk_issue_assignees, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
_ = IssueAssignee.objects.bulk_create(bulk_issue_assignees, batch_size=100, ignore_conflicts=True)
|
||||
|
||||
# Track the issue activities
|
||||
IssueActivity.objects.bulk_create(
|
||||
@@ -475,9 +428,7 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
||||
ModuleLink(
|
||||
module=module,
|
||||
url=module_data.get("link", {}).get("url", "https://plane.so"),
|
||||
title=module_data.get("link", {}).get(
|
||||
"title", "Original Issue"
|
||||
),
|
||||
title=module_data.get("link", {}).get("title", "Original Issue"),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
@@ -502,14 +453,10 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
||||
for issue in module_issues_list
|
||||
]
|
||||
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
bulk_module_issues, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
_ = ModuleIssue.objects.bulk_create(bulk_module_issues, batch_size=100, ignore_conflicts=True)
|
||||
|
||||
serializer = ModuleSerializer(modules, many=True)
|
||||
return Response(
|
||||
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response({"modules": serializer.data}, status=status.HTTP_201_CREATED)
|
||||
|
||||
else:
|
||||
return Response(
|
||||
|
||||
@@ -53,9 +53,6 @@ from plane.ee.utils.workflow import WorkflowStateManager
|
||||
from plane.ee.utils.check_user_teamspace_member import (
|
||||
check_if_current_user_is_teamspace_member,
|
||||
)
|
||||
from plane.payment.flags.flag_decorator import check_workspace_feature_flag
|
||||
from plane.ee.models import IntakeResponsibility
|
||||
from plane.payment.flags.flag import FeatureFlag
|
||||
|
||||
|
||||
class IntakeViewSet(BaseViewSet):
|
||||
|
||||
@@ -72,12 +72,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
permission_classes = [WorkSpaceAdminPermission]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("integration")
|
||||
)
|
||||
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")).select_related("integration")
|
||||
|
||||
def create(self, request, slug, provider):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
@@ -97,9 +92,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
code = request.data.get("code", False)
|
||||
|
||||
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)
|
||||
|
||||
@@ -121,9 +114,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
is_password_autoset=True,
|
||||
is_bot=True,
|
||||
first_name=integration.title,
|
||||
avatar=(
|
||||
integration.avatar_url if integration.avatar_url is not None else ""
|
||||
),
|
||||
avatar=(integration.avatar_url if integration.avatar_url is not None else ""),
|
||||
)
|
||||
|
||||
# Create an API Token for the bot user
|
||||
@@ -143,18 +134,14 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Add bot user as a member of workspace
|
||||
_ = WorkspaceMember.objects.create(
|
||||
workspace=workspace_integration.workspace, member=bot_user, role=20
|
||||
)
|
||||
_ = WorkspaceMember.objects.create(workspace=workspace_integration.workspace, member=bot_user, role=20)
|
||||
return Response(
|
||||
WorkspaceIntegrationSerializer(workspace_integration).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
pk=pk, workspace__slug=slug
|
||||
)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(pk=pk, workspace__slug=slug)
|
||||
|
||||
if workspace_integration.integration.provider == "github":
|
||||
installation_id = workspace_integration.config.get("installation_id", False)
|
||||
|
||||
@@ -28,9 +28,7 @@ class GithubRepositoriesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, workspace_integration_id):
|
||||
page = request.GET.get("page", 1)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
workspace__slug=slug, pk=workspace_integration_id
|
||||
)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(workspace__slug=slug, pk=workspace_integration_id)
|
||||
|
||||
if workspace_integration.integration.provider != "github":
|
||||
return Response(
|
||||
@@ -39,10 +37,7 @@ class GithubRepositoriesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
|
||||
repositories_url = (
|
||||
workspace_integration.metadata["repositories_url"]
|
||||
+ f"?per_page=100&page={page}"
|
||||
)
|
||||
repositories_url = workspace_integration.metadata["repositories_url"] + f"?per_page=100&page={page}"
|
||||
repositories = get_github_repos(access_tokens_url, repositories_url)
|
||||
return Response(repositories, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -78,17 +73,11 @@ class GithubRepositorySyncViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Get the workspace integration
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
pk=workspace_integration_id
|
||||
)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(pk=workspace_integration_id)
|
||||
|
||||
# Delete the old repository object
|
||||
GithubRepositorySync.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).delete()
|
||||
GithubRepository.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).delete()
|
||||
GithubRepositorySync.objects.filter(project_id=project_id, workspace__slug=slug).delete()
|
||||
GithubRepository.objects.filter(project_id=project_id, workspace__slug=slug).delete()
|
||||
|
||||
# Create repository
|
||||
repo = GithubRepository.objects.create(
|
||||
@@ -122,9 +111,7 @@ class GithubRepositorySyncViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Add bot as a member in the project
|
||||
_ = ProjectMember.objects.get_or_create(
|
||||
member=workspace_integration.actor, role=20, project_id=project_id
|
||||
)
|
||||
_ = ProjectMember.objects.get_or_create(member=workspace_integration.actor, role=20, project_id=project_id)
|
||||
|
||||
# Return Response
|
||||
return Response(
|
||||
|
||||
@@ -37,19 +37,13 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
||||
code = request.data.get("code", False)
|
||||
|
||||
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)
|
||||
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
workspace__slug=slug, pk=workspace_integration_id
|
||||
)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(workspace__slug=slug, pk=workspace_integration_id)
|
||||
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
pk=workspace_integration_id, workspace__slug=slug
|
||||
)
|
||||
workspace_integration = WorkspaceIntegration.objects.get(pk=workspace_integration_id, workspace__slug=slug)
|
||||
slack_project_sync = SlackProjectSync.objects.create(
|
||||
access_token=slack_response.get("access_token"),
|
||||
scopes=slack_response.get("scope"),
|
||||
@@ -61,9 +55,7 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
||||
workspace_integration=workspace_integration,
|
||||
project_id=project_id,
|
||||
)
|
||||
_ = ProjectMember.objects.get_or_create(
|
||||
member=workspace_integration.actor, role=20, project_id=project_id
|
||||
)
|
||||
_ = ProjectMember.objects.get_or_create(member=workspace_integration.actor, role=20, project_id=project_id)
|
||||
serializer = SlackProjectSyncSerializer(slack_project_sync)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except IntegrityError as e:
|
||||
|
||||
@@ -43,16 +43,12 @@ class LabelViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
serializer = LabelSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = LabelSerializer(data=request.data, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id)
|
||||
project_activity.delay(
|
||||
type="project.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"label": serializer.data.get("id")}, cls=DjangoJSONEncoder
|
||||
),
|
||||
requested_data=json.dumps({"label": serializer.data.get("id")}, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps({"label": None}, cls=DjangoJSONEncoder),
|
||||
@@ -104,9 +100,7 @@ class LabelViewSet(BaseViewSet):
|
||||
requested_data=json.dumps({"label": None}, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{"label": pk, "label_name": label.name}, cls=DjangoJSONEncoder
|
||||
),
|
||||
current_instance=json.dumps({"label": pk, "label_name": label.name}, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
|
||||
@@ -63,9 +63,7 @@ class IssueSubscriberViewSet(BaseViewSet):
|
||||
|
||||
def subscribe(self, request, slug, project_id, issue_id):
|
||||
if (
|
||||
IssueSubscriber.objects.filter(
|
||||
Q(issue__type__is_epic=False) | Q(issue__type__isnull=True)
|
||||
)
|
||||
IssueSubscriber.objects.filter(Q(issue__type__is_epic=False) | Q(issue__type__isnull=True))
|
||||
.filter(
|
||||
issue_id=issue_id,
|
||||
subscriber=request.user,
|
||||
|
||||
@@ -97,9 +97,7 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue.created_by == request.user
|
||||
and not check_if_current_user_is_teamspace_member(
|
||||
request.user.id, slug, project_id
|
||||
)
|
||||
and not check_if_current_user_is_teamspace_member(request.user.id, slug, project_id)
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
|
||||
@@ -512,14 +512,12 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
permission_classes = [ProjectPagePermission]
|
||||
|
||||
def retrieve(self, request, slug, project_id, page_id):
|
||||
page = (
|
||||
Page.objects.get(
|
||||
Q(owned_by=self.request.user) | Q(access=0),
|
||||
pk=page_id,
|
||||
workspace__slug=slug,
|
||||
projects__id=project_id,
|
||||
project_pages__deleted_at__isnull=True,
|
||||
)
|
||||
page = Page.objects.get(
|
||||
Q(owned_by=self.request.user) | Q(access=0),
|
||||
pk=page_id,
|
||||
workspace__slug=slug,
|
||||
projects__id=project_id,
|
||||
project_pages__deleted_at__isnull=True,
|
||||
)
|
||||
binary_data = page.description_binary
|
||||
|
||||
@@ -534,14 +532,12 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
return response
|
||||
|
||||
def partial_update(self, request, slug, project_id, page_id):
|
||||
page = (
|
||||
Page.objects.get(
|
||||
Q(owned_by=self.request.user) | Q(access=0),
|
||||
pk=page_id,
|
||||
workspace__slug=slug,
|
||||
projects__id=project_id,
|
||||
project_pages__deleted_at__isnull=True,
|
||||
)
|
||||
page = Page.objects.get(
|
||||
Q(owned_by=self.request.user) | Q(access=0),
|
||||
pk=page_id,
|
||||
workspace__slug=slug,
|
||||
projects__id=project_id,
|
||||
project_pages__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
if page.is_locked:
|
||||
|
||||
@@ -3,18 +3,15 @@ import json
|
||||
from typing import Dict, Any, List
|
||||
import uuid
|
||||
|
||||
import boto3
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Subquery
|
||||
from django.utils import timezone
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
@@ -28,7 +25,6 @@ from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
DeployBoard,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
@@ -43,7 +39,6 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
APIToken,
|
||||
)
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.utils.host import base_host
|
||||
|
||||
# EE imports
|
||||
|
||||
@@ -26,7 +26,6 @@ from plane.app.serializers import (
|
||||
from plane.app.permissions import WorkspaceUserPermission
|
||||
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
|
||||
from plane.db.models.user import BotTypeEnum
|
||||
from plane.db.models.project import get_default_preferences
|
||||
from plane.ee.models import TeamspaceMember, TeamspaceProject, PageUser
|
||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||
from plane.utils.host import base_host
|
||||
|
||||
@@ -158,9 +158,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
# Filter issues and epics by project
|
||||
if workspace_search == "false":
|
||||
issues = self.filter_issues_by_project(project_id, issues)
|
||||
issues_and_epics = self.filter_issues_by_project(
|
||||
project_id, issues_and_epics
|
||||
)
|
||||
issues_and_epics = self.filter_issues_by_project(project_id, issues_and_epics)
|
||||
|
||||
# Filter issues and epics by query
|
||||
if epic == "true":
|
||||
@@ -180,9 +178,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
issues = self.search_issues_and_excluding_parent(issues_and_epics, issue_id)
|
||||
|
||||
if issue_relation == "true" and issue_id:
|
||||
issues = self.filter_issues_excluding_related_issues(
|
||||
issue_id, issues_and_epics
|
||||
)
|
||||
issues = self.filter_issues_excluding_related_issues(issue_id, issues_and_epics)
|
||||
|
||||
if sub_issue == "true" and issue_id:
|
||||
issues = self.filter_root_issues_only(issue_id, issues)
|
||||
@@ -197,9 +193,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
issues = self.filter_issues_without_target_date(issues)
|
||||
|
||||
if convert == "true" and issue_id:
|
||||
issues = self.filter_issues_and_epics_by_excluding_given_issue_id(
|
||||
query, issue_id, issues_and_epics
|
||||
)
|
||||
issues = self.filter_issues_and_epics_by_excluding_given_issue_id(query, issue_id, issues_and_epics)
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id, member=self.request.user, is_active=True, role=5
|
||||
).exists():
|
||||
|
||||
@@ -39,9 +39,7 @@ class WorkspaceSearchEndpoint(BaseAPIView):
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
return (
|
||||
Page.objects.filter(
|
||||
q, workspace__slug=slug, archived_at__isnull=True, is_global=True
|
||||
)
|
||||
Page.objects.filter(q, workspace__slug=slug, archived_at__isnull=True, is_global=True)
|
||||
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
||||
.distinct()
|
||||
.values("name", "id", "workspace__slug")
|
||||
|
||||
@@ -263,9 +263,9 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return Issue.issue_objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug")
|
||||
).accessible_to(self.request.user.id, self.kwargs.get("slug"))
|
||||
return Issue.issue_objects.filter(workspace__slug=self.kwargs.get("slug")).accessible_to(
|
||||
self.request.user.id, self.kwargs.get("slug")
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
@@ -285,9 +285,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
issue_queryset = issue_queryset.filter(**filters)
|
||||
|
||||
if request.query_params.get("sub_issue", None) == "false":
|
||||
issue_queryset = issue_queryset.filter(
|
||||
Q(parent__isnull=True) | (Q(parent__type__is_epic=True))
|
||||
)
|
||||
issue_queryset = issue_queryset.filter(Q(parent__isnull=True) | (Q(parent__type__is_epic=True)))
|
||||
|
||||
# Get common project permission filters
|
||||
permission_filters = self._get_project_permission_filters()
|
||||
@@ -385,9 +383,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not check_if_current_user_is_teamspace_member(
|
||||
request.user.id, slug, project_id
|
||||
)
|
||||
and not check_if_current_user_is_teamspace_member(request.user.id, slug, project_id)
|
||||
):
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
@@ -413,9 +409,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
).exists()
|
||||
and not project.guest_view_all_features
|
||||
and not issue_view.owned_by == request.user
|
||||
and not check_if_current_user_is_teamspace_member(
|
||||
request.user.id, slug, project_id
|
||||
)
|
||||
and not check_if_current_user_is_teamspace_member(request.user.id, slug, project_id)
|
||||
):
|
||||
return Response(
|
||||
{"error": "You are not allowed to view this issue"},
|
||||
|
||||
@@ -123,9 +123,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
|
||||
# EE start
|
||||
if request.data.get("state_id"):
|
||||
workflow_state_manager = WorkflowStateManager(
|
||||
project_id=request.data.get("project_id", None), slug=slug
|
||||
)
|
||||
workflow_state_manager = WorkflowStateManager(project_id=request.data.get("project_id", None), slug=slug)
|
||||
if workflow_state_manager.validate_issue_creation(
|
||||
state_id=request.data.get("state_id"),
|
||||
user_id=request.user.id,
|
||||
@@ -186,12 +184,8 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
|
||||
# EE start
|
||||
# Check if state is updated then is the transition allowed
|
||||
workflow_state_manager = WorkflowStateManager(
|
||||
project_id=issue.project_id, slug=slug
|
||||
)
|
||||
if request.data.get(
|
||||
"state_id"
|
||||
) and not workflow_state_manager.validate_state_transition(
|
||||
workflow_state_manager = WorkflowStateManager(project_id=issue.project_id, slug=slug)
|
||||
if request.data.get("state_id") and not workflow_state_manager.validate_state_transition(
|
||||
issue=issue,
|
||||
new_state_id=request.data.get("state_id"),
|
||||
user_id=request.user.id,
|
||||
@@ -351,9 +345,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
draft_issue_id=None,
|
||||
)
|
||||
|
||||
draft_issue_property_values = DraftIssuePropertyValue.objects.filter(
|
||||
draft_issue=draft_issue
|
||||
)
|
||||
draft_issue_property_values = DraftIssuePropertyValue.objects.filter(draft_issue=draft_issue)
|
||||
IssuePropertyValue.objects.bulk_create(
|
||||
[
|
||||
IssuePropertyValue(
|
||||
|
||||
@@ -20,8 +20,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
# the second filter is to check if the user is a member of the project
|
||||
favorites = UserFavorite.objects.filter(user=request.user, workspace__slug=slug, parent__isnull=True).filter(
|
||||
Q(project__isnull=True) & ~Q(entity_type="page")
|
||||
| (Q(project__isnull=False))
|
||||
Q(project__isnull=True) & ~Q(entity_type="page") | (Q(project__isnull=False))
|
||||
)
|
||||
|
||||
serializer = UserFavoriteSerializer(favorites, many=True)
|
||||
|
||||
@@ -90,9 +90,9 @@ class Adapter:
|
||||
"""Check if sign up is enabled or not and raise exception if not enabled"""
|
||||
|
||||
# Get configuration value
|
||||
(ENABLE_SIGNUP,) = get_configuration_value([
|
||||
{"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")}
|
||||
])
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[{"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")}]
|
||||
)
|
||||
|
||||
# Check if sign up is disabled and invite is present or not
|
||||
if ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists():
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from celery import shared_task
|
||||
import logging
|
||||
|
||||
|
||||
@shared_task
|
||||
def app_delete_updates(application_id: str):
|
||||
"""
|
||||
@@ -17,4 +18,4 @@ def app_delete_updates(application_id: str):
|
||||
)
|
||||
for app_installation in app_installations:
|
||||
app_installation.delete()
|
||||
return
|
||||
return
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
def app_logo_asset_updates(application_id: str):
|
||||
from plane.authentication.models import Application, WorkspaceAppInstallation
|
||||
|
||||
@@ -11,9 +11,7 @@ def app_webhook_url_updates(application_id: str):
|
||||
application = Application.objects.get(id=application_id)
|
||||
if not application:
|
||||
return
|
||||
app_installations = WorkspaceAppInstallation.objects.filter(
|
||||
application_id=application_id
|
||||
)
|
||||
app_installations = WorkspaceAppInstallation.objects.filter(application_id=application_id)
|
||||
if not application.webhook_url:
|
||||
# Delete webhooks for all app installations
|
||||
for app_installation in app_installations:
|
||||
|
||||
@@ -11,7 +11,9 @@ def send_app_uninstall_webhook(webhook_url: str, workspace_id: str, application_
|
||||
"""
|
||||
|
||||
try:
|
||||
logging.getLogger("plane.worker").info(f"Sending app uninstall webhook for workspace {workspace_id} and application {application_id} and app installation {app_installation_id}")
|
||||
logging.getLogger("plane.worker").info(
|
||||
f"Sending app uninstall webhook for workspace {workspace_id} and application {application_id} and app installation {app_installation_id}"
|
||||
)
|
||||
payload = {
|
||||
"workspace_id": str(workspace_id),
|
||||
"application_id": str(application_id),
|
||||
|
||||
@@ -19,9 +19,7 @@ class OauthApplicationWorkspacePermission(BasePermission):
|
||||
|
||||
oauth2authenticated = False
|
||||
if is_authenticated:
|
||||
oauth2authenticated = isinstance(
|
||||
request.successful_authenticator, OAuth2Authentication
|
||||
)
|
||||
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
|
||||
|
||||
# If not OAuth2 authenticated, allow only if user is authenticated
|
||||
if not oauth2authenticated:
|
||||
|
||||
@@ -101,9 +101,7 @@ class GiteaOAuthProvider(OauthAdapter):
|
||||
else None
|
||||
),
|
||||
"refresh_token_expired_at": (
|
||||
datetime.fromtimestamp(
|
||||
token_response.get("refresh_token_expired_at"), tz=pytz.utc
|
||||
)
|
||||
datetime.fromtimestamp(token_response.get("refresh_token_expired_at"), tz=pytz.utc)
|
||||
if token_response.get("refresh_token_expired_at")
|
||||
else None
|
||||
),
|
||||
@@ -168,4 +166,4 @@ class GiteaOAuthProvider(OauthAdapter):
|
||||
"is_password_autoset": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -128,10 +128,11 @@ class GitHubOAuthProvider(OauthAdapter):
|
||||
return email
|
||||
except requests.RequestException:
|
||||
logger.warning(
|
||||
"Error getting email from Github", extra={
|
||||
"Error getting email from Github",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
}
|
||||
},
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
|
||||
@@ -53,19 +53,9 @@ class GoogleOAuthProvider(OauthAdapter):
|
||||
client_id = GOOGLE_CLIENT_ID
|
||||
client_secret = GOOGLE_CLIENT_SECRET
|
||||
|
||||
scheme = (
|
||||
"https"
|
||||
if settings.IS_HEROKU
|
||||
else "https"
|
||||
if request.is_secure()
|
||||
else "http"
|
||||
)
|
||||
scheme = "https" if settings.IS_HEROKU else "https" if request.is_secure() else "http"
|
||||
|
||||
redirect_uri = (
|
||||
redirect_uri
|
||||
if redirect_uri
|
||||
else (f"""{scheme}://{request.get_host()}/auth/google/callback/""")
|
||||
)
|
||||
redirect_uri = redirect_uri if redirect_uri else (f"""{scheme}://{request.get_host()}/auth/google/callback/""")
|
||||
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
|
||||
@@ -25,15 +25,14 @@ class ApplicationLinksSerializer(serializers.Serializer):
|
||||
"url": instance.get("url", ""),
|
||||
}
|
||||
|
||||
|
||||
class ApplicationSerializer(BaseSerializer):
|
||||
is_owned = serializers.BooleanField(read_only=True)
|
||||
is_installed = serializers.BooleanField(read_only=True)
|
||||
installation_id = serializers.UUIDField(read_only=True, required=False)
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
attachments_urls = serializers.SerializerMethodField()
|
||||
attachments = serializers.PrimaryKeyRelatedField(
|
||||
queryset=FileAsset.objects.all(), many=True, required=False
|
||||
)
|
||||
attachments = serializers.PrimaryKeyRelatedField(queryset=FileAsset.objects.all(), many=True, required=False)
|
||||
categories = serializers.PrimaryKeyRelatedField(
|
||||
queryset=ApplicationCategory.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
@@ -74,10 +74,7 @@ def process_workspace_project_invitations(user):
|
||||
)
|
||||
|
||||
# Sync workspace members
|
||||
[
|
||||
member_sync_task.delay(project_member_invite.workspace.slug)
|
||||
for project_member_invite in project_member_invites
|
||||
]
|
||||
[member_sync_task.delay(project_member_invite.workspace.slug) for project_member_invite in project_member_invites]
|
||||
|
||||
# Delete all the invites
|
||||
workspace_member_invites.delete()
|
||||
|
||||
@@ -37,9 +37,7 @@ class GiteaOauthInitiateEndpoint(View):
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
state = uuid.uuid4().hex
|
||||
@@ -51,9 +49,7 @@ class GiteaOauthInitiateEndpoint(View):
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -87,9 +83,7 @@ class GiteaCallbackEndpoint(View):
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = GiteaOAuthProvider(
|
||||
request=request, code=code, callback=post_user_auth_workflow
|
||||
)
|
||||
provider = GiteaOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
|
||||
@@ -21,6 +21,7 @@ from plane.utils.path_validator import get_safe_redirect_url
|
||||
|
||||
logger = logging.getLogger("plane.authentication")
|
||||
|
||||
|
||||
class GitHubOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
@@ -62,10 +63,13 @@ class GitHubCallbackEndpoint(View):
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
logger.warning("State mismatch in Github callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"State mismatch in Github callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
@@ -77,10 +81,13 @@ class GitHubCallbackEndpoint(View):
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
logger.warning("Code not found in Github callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"Code not found in Github callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
|
||||
@@ -63,10 +63,13 @@ class GitLabCallbackEndpoint(View):
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
logger.warning("State mismatch in GitLab callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"State mismatch in GitLab callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
@@ -78,10 +81,13 @@ class GitLabCallbackEndpoint(View):
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not code:
|
||||
logger.warning("Code not found in GitLab callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"Code not found in GitLab callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITLAB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITLAB_OAUTH_PROVIDER_ERROR",
|
||||
|
||||
@@ -63,10 +63,13 @@ class GoogleCallbackEndpoint(View):
|
||||
next_path = request.session.get("next_path")
|
||||
|
||||
if state != request.session.get("state", ""):
|
||||
logger.warning("State mismatch in Google callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"State mismatch in Google callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
@@ -77,10 +80,13 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
logger.warning("Code not found in Google callback", extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
})
|
||||
logger.warning(
|
||||
"Code not found in Google callback",
|
||||
extra={
|
||||
"error_code": AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
"error_message": "GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
},
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GOOGLE_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GOOGLE_OAUTH_PROVIDER_ERROR",
|
||||
|
||||
@@ -48,9 +48,7 @@ class SAMLAuthInitiateEndpoint(View):
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ class SignOutAuthEndpoint(View):
|
||||
# Check if the last medium of user is oidc
|
||||
if request.user.last_login_medium == "oidc":
|
||||
provider = OIDCOAuthProvider(request=request)
|
||||
logout_url = provider.logout(
|
||||
logout_url=f"{base_host(request=request, is_app=True)}/auth/oidc/logout/"
|
||||
)
|
||||
logout_url = provider.logout(logout_url=f"{base_host(request=request, is_app=True)}/auth/oidc/logout/")
|
||||
if logout_url:
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ class OAuthApplicationInstalledWorkspacesEndpoint(BaseAPIView):
|
||||
if request.query_params.get("id"):
|
||||
filters["id"] = request.query_params.get("id")
|
||||
|
||||
workspace_applications = WorkspaceAppInstallation.objects.filter(
|
||||
application=application, **filters
|
||||
)
|
||||
workspace_applications = WorkspaceAppInstallation.objects.filter(application=application, **filters)
|
||||
|
||||
# Always filter those workspaces where user is a member
|
||||
workspace_applications = workspace_applications.filter(
|
||||
@@ -42,9 +40,5 @@ class OAuthApplicationInstalledWorkspacesEndpoint(BaseAPIView):
|
||||
workspace=token.workspace,
|
||||
)
|
||||
|
||||
workspace_applications_serializer = WorkspaceAppInstallationSerializer(
|
||||
workspace_applications, many=True
|
||||
)
|
||||
return Response(
|
||||
workspace_applications_serializer.data, status=status.HTTP_200_OK
|
||||
)
|
||||
workspace_applications_serializer = WorkspaceAppInstallationSerializer(workspace_applications, many=True)
|
||||
return Response(workspace_applications_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -36,11 +36,7 @@ APP_INSTALLATION_ID_CACHE_TTL = 60 # 1 minute
|
||||
class OAuthTokenEndpoint(TokenView):
|
||||
"""OAuth token endpoint with rate limiting (5/minute)"""
|
||||
|
||||
@method_decorator(
|
||||
ratelimit(
|
||||
key=token_ratelimit_key, rate=TOKEN_RATE_LIMIT, block=False, group="token"
|
||||
)
|
||||
)
|
||||
@method_decorator(ratelimit(key=token_ratelimit_key, rate=TOKEN_RATE_LIMIT, block=False, group="token"))
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Check if rate limited before proceeding
|
||||
if getattr(request, "limited", False):
|
||||
@@ -53,13 +49,9 @@ class OAuthTokenEndpoint(TokenView):
|
||||
)
|
||||
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
return add_ratelimit_headers(
|
||||
request, response, TOKEN_RATE_LIMIT, token_ratelimit_key, group="token"
|
||||
)
|
||||
return add_ratelimit_headers(request, response, TOKEN_RATE_LIMIT, token_ratelimit_key, group="token")
|
||||
|
||||
def create_token_response(
|
||||
self, request: HttpRequest
|
||||
) -> Tuple[int, Dict[str, str], bytes, int]:
|
||||
def create_token_response(self, request: HttpRequest) -> Tuple[int, Dict[str, str], bytes, int]:
|
||||
token_response = super().create_token_response(request)
|
||||
_, headers, token_data, status_code = token_response
|
||||
token_data = json.loads(token_data)
|
||||
@@ -81,19 +73,15 @@ class OAuthTokenEndpoint(TokenView):
|
||||
raise exceptions.ValidationError("App installation ID is required")
|
||||
else:
|
||||
# get the workspace app installation
|
||||
workspace_app_installation = (
|
||||
WorkspaceAppInstallation.objects.filter(
|
||||
id=app_installation_id,
|
||||
application_id=application_id,
|
||||
status=WorkspaceAppInstallation.Status.INSTALLED,
|
||||
).first()
|
||||
)
|
||||
workspace_app_installation = WorkspaceAppInstallation.objects.filter(
|
||||
id=app_installation_id,
|
||||
application_id=application_id,
|
||||
status=WorkspaceAppInstallation.Status.INSTALLED,
|
||||
).first()
|
||||
# if the workspace app installation is not found, delete the token
|
||||
if not workspace_app_installation:
|
||||
token.delete()
|
||||
raise exceptions.ValidationError(
|
||||
"Workspace application not found"
|
||||
)
|
||||
raise exceptions.ValidationError("Workspace application not found")
|
||||
|
||||
# set the bot user, workspace app installation and workspace on the token
|
||||
token.user = workspace_app_installation.app_bot
|
||||
@@ -113,11 +101,7 @@ AUTHORIZE_RATE_LIMIT = "10/m"
|
||||
class CustomAuthorizationView(AuthorizationView):
|
||||
"""OAuth authorization view with rate limiting (10/minute)"""
|
||||
|
||||
@method_decorator(
|
||||
ratelimit(
|
||||
key=auth_ratelimit_key, rate=AUTHORIZE_RATE_LIMIT, block=False, group="auth"
|
||||
)
|
||||
)
|
||||
@method_decorator(ratelimit(key=auth_ratelimit_key, rate=AUTHORIZE_RATE_LIMIT, block=False, group="auth"))
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Check if rate limited before proceeding
|
||||
if getattr(request, "limited", False):
|
||||
@@ -130,9 +114,7 @@ class CustomAuthorizationView(AuthorizationView):
|
||||
)
|
||||
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
return add_ratelimit_headers(
|
||||
request, response, AUTHORIZE_RATE_LIMIT, auth_ratelimit_key, group="auth"
|
||||
)
|
||||
return add_ratelimit_headers(request, response, AUTHORIZE_RATE_LIMIT, auth_ratelimit_key, group="auth")
|
||||
|
||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||
# Redirect to login with the current URL as the next path
|
||||
@@ -185,13 +167,11 @@ class CustomOAuth2Validator(OAuth2Validator):
|
||||
# Allow 'code' response type for all applications
|
||||
if response_type == "code":
|
||||
return True
|
||||
|
||||
|
||||
# For other response types, use the default validation
|
||||
return super().validate_response_type(client_id, response_type, client, request, *args, **kwargs)
|
||||
|
||||
def validate_grant_type(
|
||||
self, client_id, grant_type, client, request, *args, **kwargs
|
||||
):
|
||||
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
|
||||
"""
|
||||
Allow both authorization_code and client_credentials for app with authorization grant type authorization_code
|
||||
and allow only client_credentials for app with authorization grant type client_credentials
|
||||
@@ -227,9 +207,7 @@ class CustomOAuth2Validator(OAuth2Validator):
|
||||
return None
|
||||
|
||||
# Get app_installation_id from cache
|
||||
app_installation_id = cache.get(
|
||||
f"app_installation_id_{client_id}_{request.user.id}"
|
||||
)
|
||||
app_installation_id = cache.get(f"app_installation_id_{client_id}_{request.user.id}")
|
||||
if app_installation_id and grant:
|
||||
try:
|
||||
# Get the workspace app installation
|
||||
@@ -255,9 +233,7 @@ class CustomOAuth2Validator(OAuth2Validator):
|
||||
"""
|
||||
Create the access token
|
||||
"""
|
||||
access_token = super()._create_access_token(
|
||||
expires, request, token, source_refresh_token
|
||||
)
|
||||
access_token = super()._create_access_token(expires, request, token, source_refresh_token)
|
||||
|
||||
# get the grant and fetch grant
|
||||
code = getattr(request, "code", None)
|
||||
@@ -283,15 +259,11 @@ class CustomOAuth2Validator(OAuth2Validator):
|
||||
access_token.save()
|
||||
return access_token
|
||||
|
||||
def _create_refresh_token(
|
||||
self, request, refresh_token_code, access_token, previous_refresh_token
|
||||
):
|
||||
def _create_refresh_token(self, request, refresh_token_code, access_token, previous_refresh_token):
|
||||
"""
|
||||
Create the refresh token
|
||||
"""
|
||||
refresh_token = super()._create_refresh_token(
|
||||
request, refresh_token_code, access_token, previous_refresh_token
|
||||
)
|
||||
refresh_token = super()._create_refresh_token(request, refresh_token_code, access_token, previous_refresh_token)
|
||||
|
||||
# Get workspace info from the access_token
|
||||
if access_token and hasattr(access_token, "workspace_app_installation"):
|
||||
|
||||
@@ -27,6 +27,4 @@ class AutomationConfig(AppConfig):
|
||||
importlib.import_module(f"{self.name}.nodes.{module_name}")
|
||||
except ImportError as e:
|
||||
# Log the error but don't fail startup
|
||||
print(
|
||||
f"Warning: Failed to import automation node module {module_name}: {e}"
|
||||
)
|
||||
print(f"Warning: Failed to import automation node module {module_name}: {e}")
|
||||
|
||||
@@ -28,9 +28,7 @@ class AutomationConsumer:
|
||||
"AUTOMATION_EVENT_STREAM_QUEUE_NAME",
|
||||
"plane.event_stream.automations",
|
||||
)
|
||||
self.exchange_name = getattr(
|
||||
settings, "AUTOMATION_EXCHANGE_NAME", "plane.event_stream"
|
||||
)
|
||||
self.exchange_name = getattr(settings, "AUTOMATION_EXCHANGE_NAME", "plane.event_stream")
|
||||
self.prefetch_count = prefetch_count
|
||||
|
||||
# Consumer state
|
||||
@@ -130,9 +128,7 @@ class AutomationConsumer:
|
||||
# Bind to fanout exchange
|
||||
channel.queue_bind(exchange=self.exchange_name, queue=self.queue_name)
|
||||
|
||||
logger.info(
|
||||
f"Queue '{self.queue_name}' bound to exchange '{self.exchange_name}'"
|
||||
)
|
||||
logger.info(f"Queue '{self.queue_name}' bound to exchange '{self.exchange_name}'")
|
||||
|
||||
def _should_process(self, event_type: str) -> bool:
|
||||
"""Check if event should be processed for automations."""
|
||||
@@ -175,22 +171,16 @@ class AutomationConsumer:
|
||||
|
||||
# Ensure exactly-once processing with retry on connection issues
|
||||
try:
|
||||
ProcessedAutomationEvent.objects.create(
|
||||
event_id=event_id, event_type=event_type, status="pending"
|
||||
)
|
||||
ProcessedAutomationEvent.objects.create(event_id=event_id, event_type=event_type, status="pending")
|
||||
except IntegrityError:
|
||||
logger.debug(f"Event {event_id} already processed")
|
||||
return True
|
||||
except OperationalError as e:
|
||||
# Retry once on database connection issues
|
||||
logger.warning(
|
||||
f"Database error creating ProcessedAutomationEvent, retrying: {e}"
|
||||
)
|
||||
logger.warning(f"Database error creating ProcessedAutomationEvent, retrying: {e}")
|
||||
self.ensure_database_connection()
|
||||
try:
|
||||
ProcessedAutomationEvent.objects.create(
|
||||
event_id=event_id, event_type=event_type, status="pending"
|
||||
)
|
||||
ProcessedAutomationEvent.objects.create(event_id=event_id, event_type=event_type, status="pending")
|
||||
except IntegrityError:
|
||||
logger.debug(f"Event {event_id} already processed on retry")
|
||||
return True
|
||||
@@ -202,24 +192,16 @@ class AutomationConsumer:
|
||||
|
||||
# Update with task_id, also with retry logic
|
||||
try:
|
||||
ProcessedAutomationEvent.objects.filter(event_id=event_id).update(
|
||||
task_id=task_result.id
|
||||
)
|
||||
ProcessedAutomationEvent.objects.filter(event_id=event_id).update(task_id=task_result.id)
|
||||
except OperationalError as e:
|
||||
logger.warning(
|
||||
f"Database error updating ProcessedAutomationEvent, retrying: {e}"
|
||||
)
|
||||
logger.warning(f"Database error updating ProcessedAutomationEvent, retrying: {e}")
|
||||
self.ensure_database_connection()
|
||||
ProcessedAutomationEvent.objects.filter(event_id=event_id).update(
|
||||
task_id=task_result.id
|
||||
)
|
||||
ProcessedAutomationEvent.objects.filter(event_id=event_id).update(task_id=task_result.id)
|
||||
|
||||
logger.info(f"Dispatched automation for {event_id} ({event_type})")
|
||||
return True
|
||||
except OperationalError as e:
|
||||
logger.warning(
|
||||
f"Database connection lost permanently, stopping consumer: {e}"
|
||||
)
|
||||
logger.warning(f"Database connection lost permanently, stopping consumer: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
@@ -248,9 +230,7 @@ class AutomationConsumer:
|
||||
if success:
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
else:
|
||||
ch.basic_nack(
|
||||
delivery_tag=method.delivery_tag, requeue=False
|
||||
)
|
||||
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
|
||||
|
||||
self._consumer_tag = channel.basic_consume(
|
||||
queue=self.queue_name, on_message_callback=message_callback
|
||||
@@ -275,9 +255,7 @@ class AutomationConsumer:
|
||||
except KeyboardInterrupt:
|
||||
self._should_stop = True
|
||||
except OperationalError:
|
||||
logger.warning(
|
||||
"Database connection lost permanently, stopping consumer"
|
||||
)
|
||||
logger.warning("Database connection lost permanently, stopping consumer")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -118,9 +118,7 @@ class AutomationExecutionEngine:
|
||||
"""Extract the triggering issue ID from the event, if present."""
|
||||
try:
|
||||
if entity_type == AutomationScopeChoices.WORKITEM:
|
||||
return event.get("entity_id") or event.get("payload", {}).get(
|
||||
"data", {}
|
||||
).get("id")
|
||||
return event.get("entity_id") or event.get("payload", {}).get("data", {}).get("id")
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
@@ -245,9 +243,7 @@ class AutomationExecutionEngine:
|
||||
automation_run=automation_run,
|
||||
)
|
||||
if not condition_result.success:
|
||||
return self._finalize_automation_failure(
|
||||
automation_run, "Condition failed", condition_result
|
||||
)
|
||||
return self._finalize_automation_failure(automation_run, "Condition failed", condition_result)
|
||||
|
||||
# Step 2: Execute action nodes linearly
|
||||
# If any action fails, stop execution and return failure
|
||||
@@ -263,16 +259,12 @@ class AutomationExecutionEngine:
|
||||
action_results.append(action_result)
|
||||
|
||||
if not action_result.success:
|
||||
return self._finalize_automation_failure(
|
||||
automation_run, f"Action {i + 1} failed", action_result
|
||||
)
|
||||
return self._finalize_automation_failure(automation_run, f"Action {i + 1} failed", action_result)
|
||||
|
||||
# All actions succeeded
|
||||
# Use the last action result for the success response
|
||||
final_result = action_results[-1] if action_results else None
|
||||
return self._finalize_automation_success(
|
||||
automation_run, "Automation completed successfully", final_result
|
||||
)
|
||||
return self._finalize_automation_success(automation_run, "Automation completed successfully", final_result)
|
||||
|
||||
def _execute_automation_after_trigger(
|
||||
self,
|
||||
@@ -307,14 +299,10 @@ class AutomationExecutionEngine:
|
||||
# TODO: We're here because the trigger node was already validated in the dispatch_automation_event function.
|
||||
validation_error = self._ensure_trigger_and_action_exist(nodes)
|
||||
if validation_error:
|
||||
return self._finalize_automation_failure(
|
||||
automation_run, validation_error
|
||||
)
|
||||
return self._finalize_automation_failure(automation_run, validation_error)
|
||||
|
||||
# Create execution context
|
||||
context = self._create_execution_context(
|
||||
automation_run=automation_run, event=event
|
||||
)
|
||||
context = self._create_execution_context(automation_run=automation_run, event=event)
|
||||
|
||||
# Record trigger execution (already successful)
|
||||
trigger_node = nodes.get(NodeTypeChoices.TRIGGER)
|
||||
@@ -336,9 +324,7 @@ class AutomationExecutionEngine:
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error during triggered automation execution")
|
||||
return self._finalize_automation_failure(
|
||||
automation_run, f"Execution error: {str(e)}"
|
||||
)
|
||||
return self._finalize_automation_failure(automation_run, f"Execution error: {str(e)}")
|
||||
|
||||
def dispatch_automation_event(self, event: dict) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -409,9 +395,7 @@ class AutomationExecutionEngine:
|
||||
)
|
||||
|
||||
# Test the trigger - NO AutomationRun created yet
|
||||
raw_trigger_result = trigger_node["handler"].execute(
|
||||
event, context.to_dict()
|
||||
)
|
||||
raw_trigger_result = trigger_node["handler"].execute(event, context.to_dict())
|
||||
|
||||
# Only proceed if trigger matches
|
||||
if not raw_trigger_result.get("success", False):
|
||||
@@ -421,11 +405,7 @@ class AutomationExecutionEngine:
|
||||
# Convert to structured result
|
||||
trigger_result = NodeResult(
|
||||
success=raw_trigger_result.get("success", False),
|
||||
output={
|
||||
k: v
|
||||
for k, v in raw_trigger_result.items()
|
||||
if k not in ["success", "error"]
|
||||
},
|
||||
output={k: v for k, v in raw_trigger_result.items() if k not in ["success", "error"]},
|
||||
error=raw_trigger_result.get("error", ""),
|
||||
)
|
||||
|
||||
@@ -434,16 +414,12 @@ class AutomationExecutionEngine:
|
||||
# Ensure DB triggers mark events as originating from automations
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT set_config('plane.initiator_type', 'SYSTEM.AUTOMATION', true)"
|
||||
)
|
||||
cur.execute("SELECT set_config('plane.initiator_type', 'SYSTEM.AUTOMATION', true)")
|
||||
except Exception:
|
||||
# Fail-safe: do not block automation if GUC is unavailable
|
||||
pass
|
||||
|
||||
entity_type = event.get(
|
||||
"entity_type", AutomationScopeChoices.WORKITEM
|
||||
)
|
||||
entity_type = event.get("entity_type", AutomationScopeChoices.WORKITEM)
|
||||
if entity_type == "issue":
|
||||
entity_type = AutomationScopeChoices.WORKITEM
|
||||
|
||||
@@ -492,9 +468,7 @@ class AutomationExecutionEngine:
|
||||
)
|
||||
|
||||
# Execute the full automation (trigger already succeeded)
|
||||
result = self._execute_automation_after_trigger(
|
||||
automation_run, event, trigger_result, nodes
|
||||
)
|
||||
result = self._execute_automation_after_trigger(automation_run, event, trigger_result, nodes)
|
||||
results.append(result.to_dict()) # Convert back to dict for API
|
||||
|
||||
except Exception as e:
|
||||
@@ -527,10 +501,7 @@ class AutomationExecutionEngine:
|
||||
|
||||
# Get all nodes for this version (optimize if not already prefetched)
|
||||
# TODO: Need to double check the node fetching logic and how it's preserving the execution order
|
||||
if (
|
||||
hasattr(version, "_prefetched_objects_cache")
|
||||
and "nodes" in version._prefetched_objects_cache
|
||||
):
|
||||
if hasattr(version, "_prefetched_objects_cache") and "nodes" in version._prefetched_objects_cache:
|
||||
# Use prefetched nodes if available
|
||||
automation_nodes = version.nodes.all()
|
||||
else:
|
||||
@@ -550,9 +521,7 @@ class AutomationExecutionEngine:
|
||||
if node.node_type == NodeTypeChoices.ACTION:
|
||||
if NodeTypeChoices.ACTION not in nodes:
|
||||
nodes[NodeTypeChoices.ACTION] = []
|
||||
nodes[NodeTypeChoices.ACTION].append(
|
||||
{"handler": node_handler, "instance": node}
|
||||
)
|
||||
nodes[NodeTypeChoices.ACTION].append({"handler": node_handler, "instance": node})
|
||||
else:
|
||||
# Store trigger and condition nodes as single items
|
||||
nodes[node.node_type] = {"handler": node_handler, "instance": node}
|
||||
@@ -604,18 +573,12 @@ class AutomationExecutionEngine:
|
||||
# Convert to structured result
|
||||
node_result = NodeResult(
|
||||
success=raw_result.get("success", False),
|
||||
output={
|
||||
k: v for k, v in raw_result.items() if k not in ["success", "error"]
|
||||
},
|
||||
output={k: v for k, v in raw_result.items() if k not in ["success", "error"]},
|
||||
error=raw_result.get("error", ""),
|
||||
)
|
||||
|
||||
# Update execution record
|
||||
node_execution.status = (
|
||||
RunStatusChoices.SUCCESS
|
||||
if node_result.success
|
||||
else RunStatusChoices.FAILED
|
||||
)
|
||||
node_execution.status = RunStatusChoices.SUCCESS if node_result.success else RunStatusChoices.FAILED
|
||||
node_execution.completed_at = timezone.now()
|
||||
node_execution.output_data = node_result.to_dict()
|
||||
node_execution.error_message = node_result.error
|
||||
@@ -625,9 +588,7 @@ class AutomationExecutionEngine:
|
||||
|
||||
except Exception as e:
|
||||
# Update execution record with error
|
||||
logger.exception(
|
||||
f"Node execution failed: {getattr(node_instance, 'name', 'unknown')}"
|
||||
)
|
||||
logger.exception(f"Node execution failed: {getattr(node_instance, 'name', 'unknown')}")
|
||||
node_execution.status = RunStatusChoices.FAILED
|
||||
node_execution.completed_at = timezone.now()
|
||||
node_execution.error_message = str(e)
|
||||
@@ -663,9 +624,7 @@ class AutomationExecutionEngine:
|
||||
NodeExecution.objects.create(
|
||||
run=automation_run,
|
||||
node=node_instance,
|
||||
status=(
|
||||
RunStatusChoices.SUCCESS if result.success else RunStatusChoices.FAILED
|
||||
),
|
||||
status=(RunStatusChoices.SUCCESS if result.success else RunStatusChoices.FAILED),
|
||||
started_at=timezone.now(), # Approximate - actual execution was earlier
|
||||
completed_at=timezone.now(),
|
||||
input_data={"event": event},
|
||||
@@ -748,40 +707,22 @@ def get_automation_status(automation_run_id: str) -> Dict[str, Any]:
|
||||
automation_run = AutomationRun.objects.get(id=automation_run_id)
|
||||
|
||||
# Get node executions
|
||||
node_executions = NodeExecution.objects.filter(
|
||||
automation_run=automation_run
|
||||
).order_by("started_at")
|
||||
node_executions = NodeExecution.objects.filter(automation_run=automation_run).order_by("started_at")
|
||||
|
||||
return {
|
||||
"automation_run_id": str(automation_run.id),
|
||||
"automation_id": str(automation_run.automation.id),
|
||||
"status": automation_run.status,
|
||||
"started_at": (
|
||||
automation_run.started_at.isoformat()
|
||||
if automation_run.started_at
|
||||
else None
|
||||
),
|
||||
"completed_at": (
|
||||
automation_run.completed_at.isoformat()
|
||||
if automation_run.completed_at
|
||||
else None
|
||||
),
|
||||
"started_at": (automation_run.started_at.isoformat() if automation_run.started_at else None),
|
||||
"completed_at": (automation_run.completed_at.isoformat() if automation_run.completed_at else None),
|
||||
"result_data": automation_run.result_data,
|
||||
"node_executions": [
|
||||
{
|
||||
"node_name": execution.node_name,
|
||||
"node_type": execution.node_type,
|
||||
"status": execution.status,
|
||||
"started_at": (
|
||||
execution.started_at.isoformat()
|
||||
if execution.started_at
|
||||
else None
|
||||
),
|
||||
"completed_at": (
|
||||
execution.completed_at.isoformat()
|
||||
if execution.completed_at
|
||||
else None
|
||||
),
|
||||
"started_at": (execution.started_at.isoformat() if execution.started_at else None),
|
||||
"completed_at": (execution.completed_at.isoformat() if execution.completed_at else None),
|
||||
"output_data": execution.output_data,
|
||||
"error_message": execution.error_message,
|
||||
}
|
||||
|
||||
@@ -70,14 +70,10 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("🚀 Automation Creation Wizard"))
|
||||
self.stdout.write(
|
||||
"Creating a linear automation: Trigger → [Condition] → Action(s)\n"
|
||||
)
|
||||
self.stdout.write("Creating a linear automation: Trigger → [Condition] → Action(s)\n")
|
||||
|
||||
if options.get("dry_run"):
|
||||
self.stdout.write(
|
||||
self.style.WARNING("🔍 DRY RUN MODE - No changes will be made\n")
|
||||
)
|
||||
self.stdout.write(self.style.WARNING("🔍 DRY RUN MODE - No changes will be made\n"))
|
||||
|
||||
# Step 1: Get workspace and project
|
||||
self._get_workspace_and_project(options)
|
||||
@@ -100,18 +96,12 @@ class Command(BaseCommand):
|
||||
# Step 7: Publish automation
|
||||
self._publish_automation()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n✅ Automation '{self.automation.name}' created successfully!"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"\n✅ Automation '{self.automation.name}' created successfully!"))
|
||||
self.stdout.write(f"Automation ID: {self.automation.id}")
|
||||
self.stdout.write(f"Version: {self.version.version_number}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.stdout.write(
|
||||
self.style.ERROR("\n❌ Automation creation cancelled by user.")
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("\n❌ Automation creation cancelled by user."))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"\n❌ Error creating automation: {e}"))
|
||||
raise
|
||||
@@ -139,9 +129,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Get project
|
||||
if not project_id:
|
||||
project_prompt = (
|
||||
f"📁 Enter Project ID (UUID) for workspace '{self.workspace.name}': "
|
||||
)
|
||||
project_prompt = f"📁 Enter Project ID (UUID) for workspace '{self.workspace.name}': "
|
||||
project_id = self._get_safe_input(
|
||||
project_prompt,
|
||||
required=True,
|
||||
@@ -152,10 +140,7 @@ class Command(BaseCommand):
|
||||
self.project = Project.objects.get(id=project_id, workspace=self.workspace)
|
||||
self.stdout.write(f"✅ Found project: {self.project.name}")
|
||||
except Project.DoesNotExist:
|
||||
error_msg = (
|
||||
f"Project with ID {project_id} not found in workspace "
|
||||
f"{self.workspace.name}"
|
||||
)
|
||||
error_msg = f"Project with ID {project_id} not found in workspace {self.workspace.name}"
|
||||
raise CommandError(error_msg)
|
||||
except ValueError:
|
||||
raise CommandError(f"Invalid project ID format: {project_id}")
|
||||
@@ -173,9 +158,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.HTTP_INFO("\n📝 Automation Details"))
|
||||
|
||||
name = self._get_safe_input("Enter automation name: ", required=True)
|
||||
description = self._get_safe_input(
|
||||
"Enter automation description (optional): ", required=False
|
||||
)
|
||||
description = self._get_safe_input("Enter automation description (optional): ", required=False)
|
||||
|
||||
# Show available scopes
|
||||
self.stdout.write("\nAvailable scopes:")
|
||||
@@ -206,11 +189,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.HTTP_INFO("\n🎯 Trigger Node"))
|
||||
|
||||
# Get available triggers
|
||||
available_triggers = {
|
||||
name: meta
|
||||
for name, meta in self.registry.all().items()
|
||||
if meta.node_type == "trigger"
|
||||
}
|
||||
available_triggers = {name: meta for name, meta in self.registry.all().items() if meta.node_type == "trigger"}
|
||||
|
||||
if not available_triggers:
|
||||
raise CommandError("No trigger handlers available")
|
||||
@@ -270,9 +249,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Get available conditions
|
||||
available_conditions = {
|
||||
name: meta
|
||||
for name, meta in self.registry.all().items()
|
||||
if meta.node_type == "condition"
|
||||
name: meta for name, meta in self.registry.all().items() if meta.node_type == "condition"
|
||||
}
|
||||
|
||||
if not available_conditions:
|
||||
@@ -326,11 +303,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.HTTP_INFO("\n⚡ Action Node(s)"))
|
||||
|
||||
# Get available actions
|
||||
available_actions = {
|
||||
name: meta
|
||||
for name, meta in self.registry.all().items()
|
||||
if meta.node_type == "action"
|
||||
}
|
||||
available_actions = {name: meta for name, meta in self.registry.all().items() if meta.node_type == "action"}
|
||||
|
||||
if not available_actions:
|
||||
raise CommandError("No action handlers available")
|
||||
@@ -343,9 +316,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write("Available actions:")
|
||||
for i, (name, meta) in enumerate(available_actions.items(), 1):
|
||||
# Get description from schema if available
|
||||
schema_info = (
|
||||
meta.schema.schema() if hasattr(meta.schema, "schema") else {}
|
||||
)
|
||||
schema_info = meta.schema.schema() if hasattr(meta.schema, "schema") else {}
|
||||
description = schema_info.get("description", "No description available")
|
||||
self.stdout.write(f" {i}. {name} - {description}")
|
||||
|
||||
@@ -485,9 +456,7 @@ class Command(BaseCommand):
|
||||
self.automation.is_enabled = True
|
||||
self.automation.save()
|
||||
|
||||
self.stdout.write(
|
||||
f"✅ Published automation version {published_version.version_number}"
|
||||
)
|
||||
self.stdout.write(f"✅ Published automation version {published_version.version_number}")
|
||||
self.stdout.write("🎉 Automation is now active!")
|
||||
|
||||
# Display summary
|
||||
@@ -497,9 +466,7 @@ class Command(BaseCommand):
|
||||
"""Display a summary of the created automation."""
|
||||
self.stdout.write(self.style.HTTP_INFO("\n📋 Automation Summary"))
|
||||
self.stdout.write(f"Name: {self.automation.name}")
|
||||
self.stdout.write(
|
||||
f"Description: {self.automation.description or 'No description'}"
|
||||
)
|
||||
self.stdout.write(f"Description: {self.automation.description or 'No description'}")
|
||||
self.stdout.write(f"Scope: {self.automation.scope}")
|
||||
self.stdout.write(f"Workspace: {self.workspace.name}")
|
||||
self.stdout.write(f"Project: {self.project.name}")
|
||||
@@ -538,9 +505,7 @@ class Command(BaseCommand):
|
||||
# Get description from schema
|
||||
has_schema = hasattr(meta.schema, "schema")
|
||||
schema_info = meta.schema.schema() if has_schema else {}
|
||||
description = schema_info.get(
|
||||
"description", "No description available"
|
||||
)
|
||||
description = schema_info.get("description", "No description available")
|
||||
|
||||
# Get required fields
|
||||
properties = schema_info.get("properties", {})
|
||||
@@ -556,10 +521,7 @@ class Command(BaseCommand):
|
||||
field_type = field_info.get("type", "string")
|
||||
field_desc = field_info.get("description", "")
|
||||
req_marker = " *" if is_required else ""
|
||||
param_line = (
|
||||
f" - {field_name} ({field_type})"
|
||||
f"{req_marker}: {field_desc}"
|
||||
)
|
||||
param_line = f" - {field_name} ({field_type}){req_marker}: {field_desc}"
|
||||
self.stdout.write(param_line)
|
||||
else:
|
||||
self.stdout.write(" Parameters: None")
|
||||
@@ -579,9 +541,7 @@ class Command(BaseCommand):
|
||||
return default
|
||||
return response in ("y", "yes") if not default else response not in ("n", "no")
|
||||
|
||||
def _get_safe_input(
|
||||
self, prompt: str, required: bool = True, validator=None
|
||||
) -> str:
|
||||
def _get_safe_input(self, prompt: str, required: bool = True, validator=None) -> str:
|
||||
"""Get user input with validation and error handling."""
|
||||
while True:
|
||||
try:
|
||||
|
||||
@@ -52,9 +52,7 @@ class Command(BaseCommand):
|
||||
"AUTOMATION_EVENT_STREAM_QUEUE_NAME",
|
||||
"plane.event_stream.automations",
|
||||
)
|
||||
exchange_name = getattr(
|
||||
settings, "AUTOMATION_EXCHANGE_NAME", "plane.event_stream"
|
||||
)
|
||||
exchange_name = getattr(settings, "AUTOMATION_EXCHANGE_NAME", "plane.event_stream")
|
||||
event_types = getattr(settings, "AUTOMATION_EVENT_TYPES", ["issue."])
|
||||
event_types_display = ", ".join(event_types)
|
||||
|
||||
@@ -77,25 +75,17 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
try:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Consumer initialized. Starting message processing..."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Consumer initialized. Starting message processing..."))
|
||||
|
||||
# Start the consumer (this will block until stopped)
|
||||
consumer.start_consuming()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.stdout.write(
|
||||
self.style.WARNING("Keyboard interrupt received, stopping...")
|
||||
)
|
||||
self.stdout.write(self.style.WARNING("Keyboard interrupt received, stopping..."))
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Unexpected error: {e}"))
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Automation consumer stopped successfully.")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Automation consumer stopped successfully."))
|
||||
|
||||
@@ -45,16 +45,11 @@ class AddCommentParams(BaseModel):
|
||||
|
||||
comment_text: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Comment text to add (supports template variables like {{payload.data.priority}})"
|
||||
),
|
||||
description=("Comment text to add (supports template variables like {{payload.data.priority}})"),
|
||||
examples=[
|
||||
"Issue priority changed to {{payload.data.priority}}",
|
||||
"Issue assigned to user ID: {{payload.data.assignee_ids.0}}",
|
||||
(
|
||||
"State changed from {{payload.previous_attributes.state_id}} "
|
||||
"to {{payload.data.state_id}}"
|
||||
),
|
||||
("State changed from {{payload.previous_attributes.state_id}} to {{payload.data.state_id}}"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -166,9 +161,7 @@ class AddCommentAction(ActionNode):
|
||||
}
|
||||
|
||||
# Render the comment template
|
||||
rendered_comment = self.render_template(
|
||||
self.params.comment_text, event, context
|
||||
)
|
||||
rendered_comment = self.render_template(self.params.comment_text, event, context)
|
||||
|
||||
if not rendered_comment.strip():
|
||||
return {
|
||||
@@ -202,17 +195,19 @@ class AddCommentAction(ActionNode):
|
||||
requested_data = IssueCommentSerializer(comment).data
|
||||
|
||||
# Ensure the activity fires only AFTER the commit
|
||||
transaction.on_commit(lambda: issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(requested_data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(automation_user_id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(issue.project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=None,
|
||||
))
|
||||
transaction.on_commit(
|
||||
lambda: issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(requested_data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(automation_user_id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(issue.project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=None,
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -230,9 +225,7 @@ class AddCommentAction(ActionNode):
|
||||
"action": "add_comment",
|
||||
}
|
||||
|
||||
def _get_assets_for_comment(
|
||||
self, issue: Issue, context: dict, automation_user_id: str
|
||||
) -> None:
|
||||
def _get_assets_for_comment(self, issue: Issue, context: dict, automation_user_id: str) -> None:
|
||||
"""
|
||||
Step 1: Extract asset ids from the FileAsset model
|
||||
Step 2: Duplicate the assets
|
||||
@@ -258,9 +251,7 @@ class AddCommentAction(ActionNode):
|
||||
)
|
||||
|
||||
if duplicated_assets:
|
||||
external_data = sync_with_external_service(
|
||||
"automation_comment", duplicated_assets
|
||||
)
|
||||
external_data = sync_with_external_service("automation_comment", duplicated_assets)
|
||||
|
||||
return {
|
||||
"comment_html": external_data.get("comment_html"),
|
||||
@@ -323,10 +314,7 @@ class ChangePropertyParams(BaseModel):
|
||||
}
|
||||
|
||||
if v not in allowed_properties:
|
||||
raise ValueError(
|
||||
f"Property '{v}' is not allowed. "
|
||||
f"Allowed properties: {allowed_properties}"
|
||||
)
|
||||
raise ValueError(f"Property '{v}' is not allowed. Allowed properties: {allowed_properties}")
|
||||
return v
|
||||
|
||||
|
||||
@@ -402,9 +390,7 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
return rendered
|
||||
|
||||
def _handle_priority_property(
|
||||
self, issue: Issue, change_type: str, values: List[Any]
|
||||
) -> Any:
|
||||
def _handle_priority_property(self, issue: Issue, change_type: str, values: List[Any]) -> Any:
|
||||
"""Handle priority property changes."""
|
||||
valid_priorities = ["urgent", "high", "medium", "low", "none"]
|
||||
|
||||
@@ -414,10 +400,7 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
new_priority = str(values[0]) if values[0] is not None else None
|
||||
if new_priority and new_priority not in valid_priorities:
|
||||
raise ValueError(
|
||||
f"Invalid priority '{new_priority}'. "
|
||||
f"Must be one of: {valid_priorities}"
|
||||
)
|
||||
raise ValueError(f"Invalid priority '{new_priority}'. Must be one of: {valid_priorities}")
|
||||
return new_priority
|
||||
|
||||
elif change_type == "remove":
|
||||
@@ -425,9 +408,7 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
return issue.priority
|
||||
|
||||
def _handle_state_property(
|
||||
self, issue: Issue, change_type: str, values: List[Any]
|
||||
) -> Any:
|
||||
def _handle_state_property(self, issue: Issue, change_type: str, values: List[Any]) -> Any:
|
||||
"""Handle state property changes."""
|
||||
if change_type in ["update", "add"]:
|
||||
if not values:
|
||||
@@ -455,13 +436,9 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
return issue.state
|
||||
|
||||
def _handle_assignees_property(
|
||||
self, issue: Issue, change_type: str, values: List[Any]
|
||||
) -> Any:
|
||||
def _handle_assignees_property(self, issue: Issue, change_type: str, values: List[Any]) -> Any:
|
||||
"""Handle assignees property changes."""
|
||||
current_assignees = [
|
||||
str(a) for a in issue.assignees.values_list("id", flat=True)
|
||||
]
|
||||
current_assignees = [str(a) for a in issue.assignees.values_list("id", flat=True)]
|
||||
current_set = set(current_assignees)
|
||||
provided_set = {str(v) for v in values if v is not None}
|
||||
|
||||
@@ -477,9 +454,7 @@ class ChangePropertyAction(ActionNode):
|
||||
"assignee_ids_to_remove": assignee_ids_to_remove,
|
||||
}
|
||||
|
||||
def _handle_labels_property(
|
||||
self, issue: Issue, change_type: str, values: List[Any]
|
||||
) -> Any:
|
||||
def _handle_labels_property(self, issue: Issue, change_type: str, values: List[Any]) -> Any:
|
||||
"""Handle labels property changes."""
|
||||
current_labels = list(issue.labels.values_list("id", flat=True))
|
||||
|
||||
@@ -492,9 +467,7 @@ class ChangePropertyAction(ActionNode):
|
||||
elif change_type == "remove":
|
||||
# Remove specified labels
|
||||
labels_to_remove = [str(v) for v in values if v is not None]
|
||||
remaining_labels = [
|
||||
label for label in current_labels if str(label) not in labels_to_remove
|
||||
]
|
||||
remaining_labels = [label for label in current_labels if str(label) not in labels_to_remove]
|
||||
return remaining_labels
|
||||
|
||||
elif change_type == "update":
|
||||
@@ -504,9 +477,7 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
return current_labels
|
||||
|
||||
def _handle_date_property(
|
||||
self, issue: Issue, change_type: str, values: List[Any], property_name: str
|
||||
) -> Any:
|
||||
def _handle_date_property(self, issue: Issue, change_type: str, values: List[Any], property_name: str) -> Any:
|
||||
"""Handle date property changes (start_date, due_date)."""
|
||||
if change_type in ["update", "add"]:
|
||||
if not values:
|
||||
@@ -524,19 +495,12 @@ class ChangePropertyAction(ActionNode):
|
||||
parsed_date = datetime.strptime(date_value, "%Y-%m-%d").date()
|
||||
return parsed_date
|
||||
else: # ISO datetime
|
||||
parsed_date = datetime.fromisoformat(
|
||||
date_value.replace("Z", "+00:00")
|
||||
).date()
|
||||
parsed_date = datetime.fromisoformat(date_value.replace("Z", "+00:00")).date()
|
||||
return parsed_date
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid date format '{date_value}'. "
|
||||
"Use YYYY-MM-DD or ISO datetime"
|
||||
)
|
||||
raise ValueError(f"Invalid date format '{date_value}'. Use YYYY-MM-DD or ISO datetime")
|
||||
else:
|
||||
logging.info(
|
||||
f"Date value is not string, returning as-is: {date_value} (type: {type(date_value)})"
|
||||
)
|
||||
logging.info(f"Date value is not string, returning as-is: {date_value} (type: {type(date_value)})")
|
||||
|
||||
return date_value
|
||||
|
||||
@@ -559,10 +523,7 @@ class ChangePropertyAction(ActionNode):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@@ -610,9 +571,7 @@ class ChangePropertyAction(ActionNode):
|
||||
"action": "change_property",
|
||||
}
|
||||
|
||||
automation_bot_user_id = Automation.objects.get(
|
||||
id=context.get("automation_id")
|
||||
).bot_user_id
|
||||
automation_bot_user_id = Automation.objects.get(id=context.get("automation_id")).bot_user_id
|
||||
issue.updated_by_id = automation_bot_user_id
|
||||
|
||||
# Render property values if they contain templates
|
||||
@@ -639,25 +598,19 @@ class ChangePropertyAction(ActionNode):
|
||||
|
||||
# Dispatch to appropriate property handler
|
||||
if property_name == "priority":
|
||||
new_value = self._handle_priority_property(
|
||||
issue, self.params.change_type, rendered_values
|
||||
)
|
||||
new_value = self._handle_priority_property(issue, self.params.change_type, rendered_values)
|
||||
issue.priority = new_value
|
||||
update_fields = ["priority", "updated_at"]
|
||||
|
||||
elif property_name == "state_id":
|
||||
new_state = self._handle_state_property(
|
||||
issue, self.params.change_type, rendered_values
|
||||
)
|
||||
new_state = self._handle_state_property(issue, self.params.change_type, rendered_values)
|
||||
# Set the state object, not just the ID
|
||||
issue.state = new_state
|
||||
new_value = str(new_state.id) if new_state else None
|
||||
update_fields = ["state_id", "updated_at"]
|
||||
|
||||
elif property_name == "assignee_ids":
|
||||
changes = self._handle_assignees_property(
|
||||
issue, self.params.change_type, rendered_values
|
||||
)
|
||||
changes = self._handle_assignees_property(issue, self.params.change_type, rendered_values)
|
||||
assignees_to_add = set(changes.get("assignee_ids_to_add", []))
|
||||
assignees_to_remove = set(changes.get("assignee_ids_to_remove", []))
|
||||
current_assignees_set = {str(a) for a in (old_value or [])}
|
||||
@@ -684,18 +637,14 @@ class ChangePropertyAction(ActionNode):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
final_assignees = list(
|
||||
(current_assignees_set - assignees_to_remove) | assignees_to_add
|
||||
)
|
||||
final_assignees = list((current_assignees_set - assignees_to_remove) | assignees_to_add)
|
||||
|
||||
old_value = str(old_value) if old_value else None
|
||||
new_value = str(final_assignees) if final_assignees else None
|
||||
update_fields = ["updated_at"]
|
||||
|
||||
elif property_name == "label_ids":
|
||||
new_label_ids = self._handle_labels_property(
|
||||
issue, self.params.change_type, rendered_values
|
||||
)
|
||||
new_label_ids = self._handle_labels_property(issue, self.params.change_type, rendered_values)
|
||||
|
||||
current_label_ids_set = {str(l) for l in (old_value or [])}
|
||||
desired_label_ids_set = {str(l) for l in (new_label_ids or [])}
|
||||
@@ -704,9 +653,7 @@ class ChangePropertyAction(ActionNode):
|
||||
labels_to_add = list(desired_label_ids_set - current_label_ids_set)
|
||||
|
||||
if labels_to_remove:
|
||||
IssueLabel.objects.using("default").filter(
|
||||
issue=issue, label_id__in=labels_to_remove
|
||||
).delete()
|
||||
IssueLabel.objects.using("default").filter(issue=issue, label_id__in=labels_to_remove).delete()
|
||||
|
||||
if labels_to_add:
|
||||
IssueLabel.objects.bulk_create(
|
||||
@@ -726,24 +673,18 @@ class ChangePropertyAction(ActionNode):
|
||||
)
|
||||
|
||||
old_value = str(old_value) if old_value else None
|
||||
new_value = (
|
||||
str(list(desired_label_ids_set)) if desired_label_ids_set else None
|
||||
)
|
||||
new_value = str(list(desired_label_ids_set)) if desired_label_ids_set else None
|
||||
update_fields = ["updated_at"]
|
||||
|
||||
elif property_name == "start_date":
|
||||
new_value = self._handle_date_property(
|
||||
issue, self.params.change_type, rendered_values, property_name
|
||||
)
|
||||
new_value = self._handle_date_property(issue, self.params.change_type, rendered_values, property_name)
|
||||
issue.start_date = new_value
|
||||
old_value = str(old_value) if old_value else None
|
||||
new_value = str(new_value) if new_value else None
|
||||
update_fields = ["start_date", "updated_at"]
|
||||
|
||||
elif property_name == "target_date":
|
||||
new_value = self._handle_date_property(
|
||||
issue, self.params.change_type, rendered_values, property_name
|
||||
)
|
||||
new_value = self._handle_date_property(issue, self.params.change_type, rendered_values, property_name)
|
||||
issue.target_date = new_value
|
||||
old_value = str(old_value) if old_value else None
|
||||
new_value = str(new_value) if new_value else None
|
||||
|
||||
@@ -46,36 +46,30 @@ class ConditionNode(BaseAutomationNode):
|
||||
def _hydrate_related_ids(self, event: dict, field_path: str) -> Any:
|
||||
"""Fetch related IDs from DB when not present in payload."""
|
||||
try:
|
||||
issue_id = event.get("entity_id") or (
|
||||
event.get("payload", {}).get("data", {}).get("id")
|
||||
)
|
||||
issue_id = event.get("entity_id") or (event.get("payload", {}).get("data", {}).get("id"))
|
||||
if not issue_id:
|
||||
return None
|
||||
|
||||
if field_path == "payload.data.assignee_ids":
|
||||
ids = list(
|
||||
IssueAssignee.objects.filter(
|
||||
issue_id=issue_id, deleted_at__isnull=True
|
||||
).values_list("assignee_id", flat=True)
|
||||
IssueAssignee.objects.filter(issue_id=issue_id, deleted_at__isnull=True).values_list(
|
||||
"assignee_id", flat=True
|
||||
)
|
||||
)
|
||||
result = [str(x) for x in ids]
|
||||
# Persist into event to avoid repeated queries
|
||||
event.setdefault("payload", {}).setdefault("data", {})[
|
||||
"assignee_ids"
|
||||
] = result
|
||||
event.setdefault("payload", {}).setdefault("data", {})["assignee_ids"] = result
|
||||
return result
|
||||
|
||||
if field_path == "payload.data.label_ids":
|
||||
ids = list(
|
||||
IssueLabel.objects.filter(
|
||||
issue_id=issue_id, deleted_at__isnull=True
|
||||
).values_list("label_id", flat=True)
|
||||
IssueLabel.objects.filter(issue_id=issue_id, deleted_at__isnull=True).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
)
|
||||
result = [str(x) for x in ids]
|
||||
# Persist into event to avoid repeated queries
|
||||
event.setdefault("payload", {}).setdefault("data", {})[
|
||||
"label_ids"
|
||||
] = result
|
||||
event.setdefault("payload", {}).setdefault("data", {})["label_ids"] = result
|
||||
return result
|
||||
|
||||
return None
|
||||
@@ -146,9 +140,7 @@ class JSONFilterParams(BaseModel):
|
||||
elif "field" in node:
|
||||
# Field comparison
|
||||
if "operator" not in node or "value" not in node:
|
||||
raise ValueError(
|
||||
"Field comparison must have 'operator' and 'value'"
|
||||
)
|
||||
raise ValueError("Field comparison must have 'operator' and 'value'")
|
||||
|
||||
valid_operators = {
|
||||
# New preferred operator (can accept single value or array)
|
||||
@@ -163,13 +155,9 @@ class JSONFilterParams(BaseModel):
|
||||
"lte",
|
||||
}
|
||||
if node["operator"] not in valid_operators:
|
||||
raise ValueError(
|
||||
f"Invalid operator '{node['operator']}'. Valid operators: {valid_operators}"
|
||||
)
|
||||
raise ValueError(f"Invalid operator '{node['operator']}'. Valid operators: {valid_operators}")
|
||||
else:
|
||||
raise ValueError(
|
||||
"Filter node must contain 'and', 'or', 'not', or 'field'"
|
||||
)
|
||||
raise ValueError("Filter node must contain 'and', 'or', 'not', or 'field'")
|
||||
|
||||
validate_node(v)
|
||||
return v
|
||||
@@ -193,9 +181,7 @@ class JSONFilterCondition(ConditionNode):
|
||||
def execute(self, event: dict, context: dict) -> Dict[str, Any]:
|
||||
"""Evaluate the JSON filter and return consistent success/failure format."""
|
||||
try:
|
||||
filter_result = self._evaluate_filter_node(
|
||||
self.params.filter_expression, event, context
|
||||
)
|
||||
filter_result = self._evaluate_filter_node(self.params.filter_expression, event, context)
|
||||
|
||||
if filter_result:
|
||||
return {
|
||||
@@ -227,28 +213,20 @@ class JSONFilterCondition(ConditionNode):
|
||||
},
|
||||
}
|
||||
|
||||
def _evaluate_filter_node(
|
||||
self, node: Dict[str, Any], event: dict, context: dict
|
||||
) -> bool:
|
||||
def _evaluate_filter_node(self, node: Dict[str, Any], event: dict, context: dict) -> bool:
|
||||
"""Recursively evaluate a filter node."""
|
||||
if "and" in node:
|
||||
conditions = node["and"]
|
||||
# Vacuous truth: AND over empty set is True
|
||||
if isinstance(conditions, list) and len(conditions) == 0:
|
||||
return True
|
||||
return all(
|
||||
self._evaluate_filter_node(condition, event, context)
|
||||
for condition in conditions
|
||||
)
|
||||
return all(self._evaluate_filter_node(condition, event, context) for condition in conditions)
|
||||
elif "or" in node:
|
||||
conditions = node["or"]
|
||||
# Identity for OR over empty set is False
|
||||
if isinstance(conditions, list) and len(conditions) == 0:
|
||||
return False
|
||||
return any(
|
||||
self._evaluate_filter_node(condition, event, context)
|
||||
for condition in conditions
|
||||
)
|
||||
return any(self._evaluate_filter_node(condition, event, context) for condition in conditions)
|
||||
elif "not" in node:
|
||||
condition = node["not"]
|
||||
if isinstance(condition, list):
|
||||
@@ -257,10 +235,7 @@ class JSONFilterCondition(ConditionNode):
|
||||
# consider empty NOT as True (no-op)
|
||||
if len(condition) == 0:
|
||||
return True
|
||||
return not all(
|
||||
self._evaluate_filter_node(cond, event, context)
|
||||
for cond in condition
|
||||
)
|
||||
return not all(self._evaluate_filter_node(cond, event, context) for cond in condition)
|
||||
return not self._evaluate_filter_node(condition, event, context)
|
||||
elif "field" in node:
|
||||
field_path = node["field"]
|
||||
@@ -271,18 +246,13 @@ class JSONFilterCondition(ConditionNode):
|
||||
else:
|
||||
return False
|
||||
|
||||
def _compare_values(
|
||||
self, field_value: Any, operator: str, expected_value: Any
|
||||
) -> bool:
|
||||
def _compare_values(self, field_value: Any, operator: str, expected_value: Any) -> bool:
|
||||
"""Compare field value against expected value using the specified operator."""
|
||||
try:
|
||||
if operator == "is":
|
||||
# Accept both single value and list of values
|
||||
if isinstance(expected_value, list):
|
||||
return any(
|
||||
self._equals_comparison(field_value, val)
|
||||
for val in expected_value
|
||||
)
|
||||
return any(self._equals_comparison(field_value, val) for val in expected_value)
|
||||
return self._equals_comparison(field_value, expected_value)
|
||||
|
||||
if operator == "equals":
|
||||
@@ -291,19 +261,14 @@ class JSONFilterCondition(ConditionNode):
|
||||
elif operator == "in":
|
||||
if not isinstance(expected_value, list):
|
||||
return False
|
||||
return any(
|
||||
self._equals_comparison(field_value, val) for val in expected_value
|
||||
)
|
||||
return any(self._equals_comparison(field_value, val) for val in expected_value)
|
||||
|
||||
elif operator == "contains":
|
||||
if field_value is None:
|
||||
return False
|
||||
# If the field value is a list, check any item's string contains expected_value
|
||||
if isinstance(field_value, list):
|
||||
return any(
|
||||
str(expected_value).lower() in str(item).lower()
|
||||
for item in field_value
|
||||
)
|
||||
return any(str(expected_value).lower() in str(item).lower() for item in field_value)
|
||||
return str(expected_value).lower() in str(field_value).lower()
|
||||
|
||||
elif operator in ["gt", "gte", "lt", "lte"]:
|
||||
@@ -322,12 +287,8 @@ class JSONFilterCondition(ConditionNode):
|
||||
|
||||
# If either side is a list, treat as set-like overlap: any pair equals
|
||||
if isinstance(field_value, list) or isinstance(expected_value, list):
|
||||
field_items = (
|
||||
field_value if isinstance(field_value, list) else [field_value]
|
||||
)
|
||||
expected_items = (
|
||||
expected_value if isinstance(expected_value, list) else [expected_value]
|
||||
)
|
||||
field_items = field_value if isinstance(field_value, list) else [field_value]
|
||||
expected_items = expected_value if isinstance(expected_value, list) else [expected_value]
|
||||
for field_item in field_items:
|
||||
for expected_item in expected_items:
|
||||
if self._equals_comparison(field_item, expected_item):
|
||||
@@ -339,17 +300,13 @@ class JSONFilterCondition(ConditionNode):
|
||||
return self._date_equals_comparison(field_value, expected_value)
|
||||
|
||||
# Handle numeric comparisons (int/float)
|
||||
if isinstance(field_value, (int, float)) and isinstance(
|
||||
expected_value, (int, float)
|
||||
):
|
||||
if isinstance(field_value, (int, float)) and isinstance(expected_value, (int, float)):
|
||||
return field_value == expected_value
|
||||
|
||||
# String comparison (case-insensitive)
|
||||
return str(field_value).lower() == str(expected_value).lower()
|
||||
|
||||
def _numeric_comparison(
|
||||
self, field_value: Any, operator: str, expected_value: Any
|
||||
) -> bool:
|
||||
def _numeric_comparison(self, field_value: Any, operator: str, expected_value: Any) -> bool:
|
||||
"""Perform numeric comparison with type coercion."""
|
||||
try:
|
||||
# Handle date comparisons
|
||||
@@ -377,9 +334,7 @@ class JSONFilterCondition(ConditionNode):
|
||||
|
||||
return False
|
||||
|
||||
def _date_comparison(
|
||||
self, field_value: Any, operator: str, expected_value: Any
|
||||
) -> bool:
|
||||
def _date_comparison(self, field_value: Any, operator: str, expected_value: Any) -> bool:
|
||||
"""Perform date comparison with flexible parsing."""
|
||||
try:
|
||||
field_date = self._parse_date(field_value)
|
||||
|
||||
@@ -48,9 +48,7 @@ class NodeRegistry:
|
||||
# ---------------------------------------------------------------------
|
||||
# API
|
||||
# ---------------------------------------------------------------------
|
||||
def register(
|
||||
self, name: str, node_type: str, handler: Callable, schema: Type[BaseModel]
|
||||
):
|
||||
def register(self, name: str, node_type: str, handler: Callable, schema: Type[BaseModel]):
|
||||
if name in self._registry:
|
||||
raise ValueError(f"Node '{name}' already registered")
|
||||
self._registry[name] = NodeMeta(name, node_type, handler, schema)
|
||||
|
||||
@@ -64,9 +64,7 @@ def execute_automation_task(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
with transaction.atomic():
|
||||
# Mark event as processing
|
||||
# TODO: Do we need this logic? (might be required when we have multiple consumers running)
|
||||
updated_count = ProcessedAutomationEvent.mark_processing(
|
||||
event_id=event_id, task_id=self.request.id
|
||||
)
|
||||
updated_count = ProcessedAutomationEvent.mark_processing(event_id=event_id, task_id=self.request.id)
|
||||
|
||||
if updated_count == 0:
|
||||
# Event might have been processed by another task or doesn't exist
|
||||
@@ -85,9 +83,7 @@ def execute_automation_task(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
# Check if any automations were triggered
|
||||
if automation_results and len(automation_results) > 0:
|
||||
logger.info(
|
||||
f"Event {event_id} triggered {len(automation_results)} automations"
|
||||
)
|
||||
logger.info(f"Event {event_id} triggered {len(automation_results)} automations")
|
||||
|
||||
# Mark as completed
|
||||
ProcessedAutomationEvent.mark_completed(event_id)
|
||||
@@ -96,9 +92,7 @@ def execute_automation_task(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"success": True,
|
||||
"event_id": event_id,
|
||||
"automations_triggered": len(automation_results),
|
||||
"automation_runs": [
|
||||
result.get("automation_run_id") for result in automation_results
|
||||
],
|
||||
"automation_runs": [result.get("automation_run_id") for result in automation_results],
|
||||
}
|
||||
else:
|
||||
# No automations triggered, but still successful
|
||||
@@ -119,9 +113,7 @@ def execute_automation_task(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
# Mark as failed
|
||||
try:
|
||||
ProcessedAutomationEvent.mark_failed(
|
||||
event_id=event_id, error_message=str(e), increment_retry=True
|
||||
)
|
||||
ProcessedAutomationEvent.mark_failed(event_id=event_id, error_message=str(e), increment_retry=True)
|
||||
except Exception as mark_error:
|
||||
logger.error(f"Failed to mark event {event_id} as failed: {mark_error}")
|
||||
|
||||
|
||||
@@ -34,10 +34,7 @@ def create_workspace_members(workspace, members):
|
||||
members = User.objects.filter(email__in=members)
|
||||
|
||||
_ = WorkspaceMember.objects.bulk_create(
|
||||
[
|
||||
WorkspaceMember(workspace=workspace, member=member, role=20)
|
||||
for member in members
|
||||
],
|
||||
[WorkspaceMember(workspace=workspace, member=member, role=20) for member in members],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
return
|
||||
@@ -49,9 +46,7 @@ def create_project(workspace, user_id):
|
||||
project = Project.objects.create(
|
||||
workspace=workspace,
|
||||
name=name,
|
||||
identifier=name[
|
||||
: random.randint(2, 12 if len(name) - 1 >= 12 else len(name) - 1)
|
||||
].upper(),
|
||||
identifier=name[: random.randint(2, 12 if len(name) - 1 >= 12 else len(name) - 1)].upper(),
|
||||
created_by_id=user_id,
|
||||
)
|
||||
|
||||
@@ -167,17 +162,11 @@ def create_cycles(workspace, project, user_id, cycle_count):
|
||||
)
|
||||
|
||||
# Ensure end_date is strictly after start_date if start_date is not None
|
||||
while start_date is not None and (
|
||||
end_date <= start_date or (start_date, end_date) in used_date_ranges
|
||||
):
|
||||
while start_date is not None and (end_date <= start_date or (start_date, end_date) in used_date_ranges):
|
||||
end_date = fake.date_this_year()
|
||||
|
||||
# Add the unique date range to the set
|
||||
(
|
||||
used_date_ranges.add((start_date, end_date))
|
||||
if (end_date is not None and start_date is not None)
|
||||
else None
|
||||
)
|
||||
(used_date_ranges.add((start_date, end_date)) if (end_date is not None and start_date is not None) else None)
|
||||
|
||||
# Append the cycle with unique date range
|
||||
cycles.append(
|
||||
@@ -235,9 +224,7 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
issues = []
|
||||
|
||||
# Get the maximum sequence_id
|
||||
last_id = IssueSequence.objects.filter(project=project).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
last_id = IssueSequence.objects.filter(project=project).aggregate(largest=Max("sequence"))["largest"]
|
||||
|
||||
last_id = 1 if last_id is None else last_id + 1
|
||||
|
||||
@@ -246,9 +233,7 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
project=project, state_id=states[random.randint(0, len(states) - 1)]
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
|
||||
largest_sort_order = (
|
||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
)
|
||||
largest_sort_order = 65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
|
||||
for _ in range(0, issue_count):
|
||||
start_date = [None, fake.date_this_year()][random.randint(0, 1)]
|
||||
@@ -274,9 +259,7 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
sort_order=largest_sort_order,
|
||||
start_date=start_date,
|
||||
target_date=end_date,
|
||||
priority=["urgent", "high", "medium", "low", "none"][
|
||||
random.randint(0, 4)
|
||||
],
|
||||
priority=["urgent", "high", "medium", "low", "none"][random.randint(0, 4)],
|
||||
created_by_id=creators[random.randint(0, len(creators) - 1)],
|
||||
)
|
||||
)
|
||||
@@ -321,12 +304,8 @@ def create_issues(workspace, project, user_id, issue_count):
|
||||
def create_issue_parent(workspace, project, user_id, issue_count):
|
||||
parent_count = issue_count / 4
|
||||
|
||||
parent_issues = Issue.objects.filter(project=project).values_list("id", flat=True)[
|
||||
: int(parent_count)
|
||||
]
|
||||
sub_issues = Issue.objects.filter(project=project).exclude(pk__in=parent_issues)[
|
||||
: int(issue_count / 2)
|
||||
]
|
||||
parent_issues = Issue.objects.filter(project=project).values_list("id", flat=True)[: int(parent_count)]
|
||||
sub_issues = Issue.objects.filter(project=project).exclude(pk__in=parent_issues)[: int(issue_count / 2)]
|
||||
|
||||
bulk_sub_issues = []
|
||||
for sub_issue in sub_issues:
|
||||
@@ -337,9 +316,7 @@ def create_issue_parent(workspace, project, user_id, issue_count):
|
||||
|
||||
def create_issue_assignees(workspace, project, user_id, issue_count):
|
||||
# assignees
|
||||
assignees = ProjectMember.objects.filter(project=project).values_list(
|
||||
"member_id", flat=True
|
||||
)
|
||||
assignees = ProjectMember.objects.filter(project=project).values_list("member_id", flat=True)
|
||||
issues = random.sample(
|
||||
list(Issue.objects.filter(project=project).values_list("id", flat=True)),
|
||||
int(issue_count / 2),
|
||||
@@ -348,9 +325,7 @@ def create_issue_assignees(workspace, project, user_id, issue_count):
|
||||
# Bulk issue
|
||||
bulk_issue_assignees = []
|
||||
for issue in issues:
|
||||
for assignee in random.sample(
|
||||
list(assignees), random.randint(0, len(assignees) - 1)
|
||||
):
|
||||
for assignee in random.sample(list(assignees), random.randint(0, len(assignees) - 1)):
|
||||
bulk_issue_assignees.append(
|
||||
IssueAssignee(
|
||||
issue_id=issue,
|
||||
@@ -361,9 +336,7 @@ def create_issue_assignees(workspace, project, user_id, issue_count):
|
||||
)
|
||||
|
||||
# Issue assignees
|
||||
IssueAssignee.objects.bulk_create(
|
||||
bulk_issue_assignees, batch_size=1000, ignore_conflicts=True
|
||||
)
|
||||
IssueAssignee.objects.bulk_create(bulk_issue_assignees, batch_size=1000, ignore_conflicts=True)
|
||||
|
||||
|
||||
def create_issue_labels(workspace, project, user_id, issue_count):
|
||||
@@ -378,16 +351,10 @@ def create_issue_labels(workspace, project, user_id, issue_count):
|
||||
bulk_issue_labels = []
|
||||
for issue in issues:
|
||||
for label in random.sample(list(labels), random.randint(0, len(labels) - 1)):
|
||||
bulk_issue_labels.append(
|
||||
IssueLabel(
|
||||
issue_id=issue, label_id=label, project=project, workspace=workspace
|
||||
)
|
||||
)
|
||||
bulk_issue_labels.append(IssueLabel(issue_id=issue, label_id=label, project=project, workspace=workspace))
|
||||
|
||||
# Issue assignees
|
||||
IssueLabel.objects.bulk_create(
|
||||
bulk_issue_labels, batch_size=1000, ignore_conflicts=True
|
||||
)
|
||||
IssueLabel.objects.bulk_create(bulk_issue_labels, batch_size=1000, ignore_conflicts=True)
|
||||
|
||||
|
||||
def create_cycle_issues(workspace, project, user_id, issue_count):
|
||||
@@ -402,16 +369,10 @@ def create_cycle_issues(workspace, project, user_id, issue_count):
|
||||
bulk_cycle_issues = []
|
||||
for issue in issues:
|
||||
cycle = cycles[random.randint(0, len(cycles) - 1)]
|
||||
bulk_cycle_issues.append(
|
||||
CycleIssue(
|
||||
cycle_id=cycle, issue_id=issue, project=project, workspace=workspace
|
||||
)
|
||||
)
|
||||
bulk_cycle_issues.append(CycleIssue(cycle_id=cycle, issue_id=issue, project=project, workspace=workspace))
|
||||
|
||||
# Issue assignees
|
||||
CycleIssue.objects.bulk_create(
|
||||
bulk_cycle_issues, batch_size=1000, ignore_conflicts=True
|
||||
)
|
||||
CycleIssue.objects.bulk_create(bulk_cycle_issues, batch_size=1000, ignore_conflicts=True)
|
||||
|
||||
|
||||
def create_module_issues(workspace, project, user_id, issue_count):
|
||||
@@ -426,15 +387,9 @@ def create_module_issues(workspace, project, user_id, issue_count):
|
||||
bulk_module_issues = []
|
||||
for issue in issues:
|
||||
module = modules[random.randint(0, len(modules) - 1)]
|
||||
bulk_module_issues.append(
|
||||
ModuleIssue(
|
||||
module_id=module, issue_id=issue, project=project, workspace=workspace
|
||||
)
|
||||
)
|
||||
bulk_module_issues.append(ModuleIssue(module_id=module, issue_id=issue, project=project, workspace=workspace))
|
||||
# Issue assignees
|
||||
ModuleIssue.objects.bulk_create(
|
||||
bulk_module_issues, batch_size=1000, ignore_conflicts=True
|
||||
)
|
||||
ModuleIssue.objects.bulk_create(bulk_module_issues, batch_size=1000, ignore_conflicts=True)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -471,52 +426,36 @@ def create_fake_data(slug, email, members, issue_count, cycle_count, module_coun
|
||||
|
||||
# create cycles
|
||||
print("Creating cycles")
|
||||
_ = create_cycles(
|
||||
workspace=workspace, project=project, user_id=user_id, cycle_count=cycle_count
|
||||
)
|
||||
_ = create_cycles(workspace=workspace, project=project, user_id=user_id, cycle_count=cycle_count)
|
||||
print("Done creating cycles")
|
||||
|
||||
# create modules
|
||||
print("Creating modules")
|
||||
_ = create_modules(
|
||||
workspace=workspace, project=project, user_id=user_id, module_count=module_count
|
||||
)
|
||||
_ = create_modules(workspace=workspace, project=project, user_id=user_id, module_count=module_count)
|
||||
print("Done creating modules")
|
||||
|
||||
print("Creating issues")
|
||||
create_issues(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_issues(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating issues")
|
||||
|
||||
print("Creating parent and sub issues")
|
||||
create_issue_parent(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_issue_parent(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating parent and sub issues")
|
||||
|
||||
print("Creating issue assignees")
|
||||
create_issue_assignees(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_issue_assignees(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating issue assignees")
|
||||
|
||||
print("Creating issue labels")
|
||||
create_issue_labels(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_issue_labels(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating issue labels")
|
||||
|
||||
print("Creating cycle issues")
|
||||
create_cycle_issues(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_cycle_issues(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating cycle issues")
|
||||
|
||||
print("Creating module issues")
|
||||
create_module_issues(
|
||||
workspace=workspace, project=project, user_id=user_id, issue_count=issue_count
|
||||
)
|
||||
create_module_issues(workspace=workspace, project=project, user_id=user_id, issue_count=issue_count)
|
||||
print("Done creating module issues")
|
||||
|
||||
return
|
||||
|
||||
@@ -35,36 +35,37 @@ def stack_email_notification():
|
||||
email_notifications = EmailNotificationLog.objects.filter(processed_at__isnull=True).order_by("receiver").values()
|
||||
|
||||
# Group by receiver_id AND entity_name to handle different entity types separately
|
||||
receivers_entities = list(set([
|
||||
(str(notification.get("receiver_id")), notification.get("entity_name"))
|
||||
for notification in email_notifications
|
||||
]))
|
||||
|
||||
receivers_entities = list(
|
||||
set(
|
||||
[
|
||||
(str(notification.get("receiver_id")), notification.get("entity_name"))
|
||||
for notification in email_notifications
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
processed_notifications = []
|
||||
|
||||
for receiver_id, entity_name in receivers_entities:
|
||||
# Get notifications for this specific receiver AND entity_name combination
|
||||
receiver_notifications = [
|
||||
notification for notification in email_notifications
|
||||
if str(notification.get("receiver_id")) == receiver_id
|
||||
and notification.get("entity_name") == entity_name
|
||||
notification
|
||||
for notification in email_notifications
|
||||
if str(notification.get("receiver_id")) == receiver_id and notification.get("entity_name") == entity_name
|
||||
]
|
||||
|
||||
|
||||
# Create payload for this entity type only
|
||||
payload = {}
|
||||
entity_notification_ids = {}
|
||||
|
||||
|
||||
for receiver_notification in receiver_notifications:
|
||||
entity_identifier = receiver_notification.get("entity_identifier")
|
||||
payload.setdefault(receiver_notification.get("entity_identifier"), {}).setdefault(
|
||||
str(receiver_notification.get("triggered_by_id")), []
|
||||
).append(receiver_notification.get("data"))
|
||||
|
||||
|
||||
# Track processed notifications and IDs
|
||||
entity_notification_ids.setdefault(entity_identifier, []).append(
|
||||
receiver_notification.get("id")
|
||||
)
|
||||
entity_notification_ids.setdefault(entity_identifier, []).append(receiver_notification.get("id"))
|
||||
|
||||
# Track processed notifications for this entity
|
||||
processed_notifications.append(receiver_notification.get("id"))
|
||||
@@ -98,6 +99,7 @@ def stack_email_notification():
|
||||
# Update the email notification log
|
||||
EmailNotificationLog.objects.filter(pk__in=processed_notifications).update(processed_at=timezone.now())
|
||||
|
||||
|
||||
def create_payload(notification_data, entity_name):
|
||||
# return format {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
|
||||
data = {}
|
||||
@@ -366,8 +368,6 @@ def send_workspace_level_email_notification(
|
||||
logging.getLogger("plane.worker").warning(f"Entity not found: {entity_id} for entity name: {entity_name}")
|
||||
return
|
||||
|
||||
|
||||
|
||||
actors_involved = []
|
||||
template_data = []
|
||||
|
||||
@@ -458,4 +458,4 @@ def send_email(subject, text_content, receiver, html_content, email_notification
|
||||
msg.send()
|
||||
logging.getLogger("plane.worker").info("Email Sent Successfully")
|
||||
|
||||
EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(sent_at=timezone.now())
|
||||
EmailNotificationLog.objects.filter(pk__in=email_notification_ids).update(sent_at=timezone.now())
|
||||
|
||||
@@ -66,11 +66,8 @@ def share_page_notification(page_id, user_id, newly_shared_user_ids, slug):
|
||||
"page_url": page_url,
|
||||
"workspace": workspace,
|
||||
"page_name": page.name,
|
||||
"page_description": page.description_stripped
|
||||
or "No description available",
|
||||
"shared_by_name": f"{user.first_name} {user.last_name}".strip()
|
||||
or user.display_name
|
||||
or user.email,
|
||||
"page_description": page.description_stripped or "No description available",
|
||||
"shared_by_name": f"{user.first_name} {user.last_name}".strip() or user.display_name or user.email,
|
||||
"shared_to_name": (
|
||||
f"{newly_shared_user.first_name} {newly_shared_user.last_name}".strip() # noqa: E501
|
||||
or newly_shared_user.display_name
|
||||
@@ -82,9 +79,7 @@ def share_page_notification(page_id, user_id, newly_shared_user_ids, slug):
|
||||
|
||||
# Create email subject and content
|
||||
subject = f"{context['shared_by_name']} shared a page with you"
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/share_page.html", context
|
||||
)
|
||||
html_content = render_to_string("emails/notifications/share_page.html", context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
# Configure email connection
|
||||
|
||||
@@ -71,8 +71,7 @@ def service_importer(service, importer_id):
|
||||
email__in=[
|
||||
user.get("email").strip().lower()
|
||||
for user in users
|
||||
if user.get("import", False) == "invite"
|
||||
or user.get("import", False) == "map"
|
||||
if user.get("import", False) == "invite" or user.get("import", False) == "map"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -144,9 +143,7 @@ def service_importer(service, importer_id):
|
||||
GithubRepository.objects.filter(project_id=importer.project_id).delete()
|
||||
|
||||
# Create a Label for github
|
||||
label = Label.objects.filter(
|
||||
name="GitHub", project_id=importer.project_id
|
||||
).first()
|
||||
label = Label.objects.filter(name="GitHub", project_id=importer.project_id).first()
|
||||
|
||||
if label is None:
|
||||
label = Label.objects.create(
|
||||
|
||||
@@ -39,6 +39,7 @@ from plane.utils.issue_relation_mapper import get_inverse_relation
|
||||
from plane.utils.uuid import is_valid_uuid
|
||||
from plane.bgtasks.notification_task import process_workitem_notifications
|
||||
|
||||
|
||||
def extract_ids(data: dict | None, primary_key: str, fallback_key: str) -> set[str]:
|
||||
if not data:
|
||||
return set()
|
||||
@@ -86,14 +87,8 @@ def track_description(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
):
|
||||
last_activity = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if current_instance.get("description_html") != requested_data.get("description_html"):
|
||||
last_activity = IssueActivity.objects.filter(issue_id=issue_id).order_by("-created_at").first()
|
||||
if (
|
||||
last_activity is not None
|
||||
and last_activity.field == "description"
|
||||
@@ -150,12 +145,8 @@ def track_parent(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_parent_id = current_instance.get("parent_id") or current_instance.get(
|
||||
"parent"
|
||||
)
|
||||
requested_parent_id = requested_data.get("parent_id") or requested_data.get(
|
||||
"parent"
|
||||
)
|
||||
current_parent_id = current_instance.get("parent_id") or current_instance.get("parent")
|
||||
requested_parent_id = requested_data.get("parent_id") or requested_data.get("parent")
|
||||
|
||||
# Validate UUIDs before database queries
|
||||
if current_parent_id is not None and not is_valid_uuid(current_parent_id):
|
||||
@@ -164,16 +155,8 @@ def track_parent(
|
||||
return
|
||||
|
||||
if current_parent_id != requested_parent_id:
|
||||
old_parent = (
|
||||
Issue.objects.filter(pk=current_parent_id).first()
|
||||
if current_parent_id is not None
|
||||
else None
|
||||
)
|
||||
new_parent = (
|
||||
Issue.objects.filter(pk=requested_parent_id).first()
|
||||
if requested_parent_id is not None
|
||||
else None
|
||||
)
|
||||
old_parent = Issue.objects.filter(pk=current_parent_id).first() if current_parent_id is not None else None
|
||||
new_parent = Issue.objects.filter(pk=requested_parent_id).first() if requested_parent_id is not None else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -181,14 +164,10 @@ def track_parent(
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
f"{old_parent.project.identifier}-{old_parent.sequence_id}"
|
||||
if old_parent is not None
|
||||
else ""
|
||||
f"{old_parent.project.identifier}-{old_parent.sequence_id}" if old_parent is not None else ""
|
||||
),
|
||||
new_value=(
|
||||
f"{new_parent.project.identifier}-{new_parent.sequence_id}"
|
||||
if new_parent is not None
|
||||
else ""
|
||||
f"{new_parent.project.identifier}-{new_parent.sequence_id}" if new_parent is not None else ""
|
||||
),
|
||||
field="parent",
|
||||
project_id=project_id,
|
||||
@@ -249,12 +228,8 @@ def track_state(
|
||||
requested_state_id = None
|
||||
|
||||
if current_state_id != requested_state_id:
|
||||
new_state = State.objects.filter(
|
||||
pk=requested_state_id, project_id=project_id
|
||||
).first()
|
||||
old_state = State.objects.filter(
|
||||
pk=current_state_id, project_id=project_id
|
||||
).first()
|
||||
new_state = State.objects.filter(pk=requested_state_id, project_id=project_id).first()
|
||||
old_state = State.objects.filter(pk=current_state_id, project_id=project_id).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -292,15 +267,9 @@ def track_target_date(
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
current_instance.get("target_date")
|
||||
if current_instance.get("target_date") is not None
|
||||
else ""
|
||||
),
|
||||
new_value=(
|
||||
requested_data.get("target_date")
|
||||
if requested_data.get("target_date") is not None
|
||||
else ""
|
||||
current_instance.get("target_date") if current_instance.get("target_date") is not None else ""
|
||||
),
|
||||
new_value=(requested_data.get("target_date") if requested_data.get("target_date") is not None else ""),
|
||||
field="target_date",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
@@ -328,15 +297,9 @@ def track_start_date(
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
current_instance.get("start_date")
|
||||
if current_instance.get("start_date") is not None
|
||||
else ""
|
||||
),
|
||||
new_value=(
|
||||
requested_data.get("start_date")
|
||||
if requested_data.get("start_date") is not None
|
||||
else ""
|
||||
current_instance.get("start_date") if current_instance.get("start_date") is not None else ""
|
||||
),
|
||||
new_value=(requested_data.get("start_date") if requested_data.get("start_date") is not None else ""),
|
||||
field="start_date",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
@@ -465,9 +428,7 @@ def track_assignees(
|
||||
)
|
||||
|
||||
# Create assignees subscribers to the issue and ignore if already
|
||||
IssueSubscriber.objects.bulk_create(
|
||||
bulk_subscribers, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
IssueSubscriber.objects.bulk_create(bulk_subscribers, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
for dropped_assignee in dropped_assginees:
|
||||
# validate uuids
|
||||
@@ -504,16 +465,12 @@ def track_estimate_points(
|
||||
):
|
||||
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
||||
old_estimate = (
|
||||
EstimatePoint.objects.filter(
|
||||
pk=current_instance.get("estimate_point")
|
||||
).first()
|
||||
EstimatePoint.objects.filter(pk=current_instance.get("estimate_point")).first()
|
||||
if current_instance.get("estimate_point") is not None
|
||||
else None
|
||||
)
|
||||
new_estimate = (
|
||||
EstimatePoint.objects.filter(
|
||||
pk=requested_data.get("estimate_point")
|
||||
).first()
|
||||
EstimatePoint.objects.filter(pk=requested_data.get("estimate_point")).first()
|
||||
if requested_data.get("estimate_point") is not None
|
||||
else None
|
||||
)
|
||||
@@ -535,9 +492,7 @@ def track_estimate_points(
|
||||
else None
|
||||
),
|
||||
new_identifier=(
|
||||
requested_data.get("estimate_point")
|
||||
if requested_data.get("estimate_point") is not None
|
||||
else None
|
||||
requested_data.get("estimate_point") if requested_data.get("estimate_point") is not None else None
|
||||
),
|
||||
old_value=old_estimate.value if old_estimate else None,
|
||||
new_value=new_estimate.value if new_estimate else None,
|
||||
@@ -610,9 +565,7 @@ def track_closed_to(
|
||||
epoch,
|
||||
):
|
||||
if requested_data.get("closed_to") is not None:
|
||||
updated_state = State.objects.get(
|
||||
pk=requested_data.get("closed_to"), project_id=project_id
|
||||
)
|
||||
updated_state = State.objects.get(pk=requested_data.get("closed_to"), project_id=project_id)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
@@ -646,12 +599,8 @@ def track_type(
|
||||
|
||||
if new_type_id != old_type_id:
|
||||
verb = "updated" if new_type_id else "deleted"
|
||||
old_type = (
|
||||
IssueType.objects.filter(pk=old_type_id).first() if old_type_id else None
|
||||
)
|
||||
new_type = (
|
||||
IssueType.objects.filter(pk=new_type_id).first() if new_type_id else None
|
||||
)
|
||||
old_type = IssueType.objects.filter(pk=old_type_id).first() if old_type_id else None
|
||||
new_type = IssueType.objects.filter(pk=new_type_id).first() if new_type_id else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -741,9 +690,7 @@ def update_issue_activity(
|
||||
}
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key)
|
||||
@@ -795,9 +742,7 @@ def create_comment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -827,9 +772,7 @@ def update_comment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
if current_instance.get("comment_html") != requested_data.get("comment_html"):
|
||||
issue_activities.append(
|
||||
@@ -888,21 +831,15 @@ def create_cycle_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_cycle_issues", [])
|
||||
created_records = json.loads(current_instance.get("created_cycle_issues", []))
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("old_cycle_id", None)
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("new_cycle_id", None)
|
||||
).first()
|
||||
old_cycle = Cycle.objects.filter(pk=updated_record.get("old_cycle_id", None)).first()
|
||||
new_cycle = Cycle.objects.filter(pk=updated_record.get("new_cycle_id", None)).first()
|
||||
issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
@@ -927,12 +864,8 @@ def create_cycle_issue_activity(
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
cycle = Cycle.objects.filter(
|
||||
pk=created_record.get("fields").get("cycle")
|
||||
).first()
|
||||
issue = Issue.objects.filter(
|
||||
pk=created_record.get("fields").get("issue")
|
||||
).first()
|
||||
cycle = Cycle.objects.filter(pk=created_record.get("fields").get("cycle")).first()
|
||||
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
@@ -965,9 +898,7 @@ def delete_cycle_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
cycle_id = requested_data.get("cycle_id", "")
|
||||
cycle_name = requested_data.get("cycle_name", "")
|
||||
@@ -1039,9 +970,7 @@ def delete_module_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
module_name = current_instance.get("module_name")
|
||||
current_issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if current_issue:
|
||||
@@ -1058,11 +987,7 @@ def delete_module_issue_activity(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"removed this issue from {module_name}",
|
||||
old_identifier=(
|
||||
requested_data.get("module_id")
|
||||
if requested_data.get("module_id") is not None
|
||||
else None
|
||||
),
|
||||
old_identifier=(requested_data.get("module_id") if requested_data.get("module_id") is not None else None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
@@ -1112,9 +1037,7 @@ def update_milestone_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
milestone = Milestone.objects.filter(pk=requested_data.get("milestone_id")).first()
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
@@ -1149,9 +1072,7 @@ def delete_milestone_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
milestone_name = current_instance.get("milestone_name")
|
||||
current_issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if current_issue:
|
||||
@@ -1169,9 +1090,7 @@ def delete_milestone_issue_activity(
|
||||
workspace_id=workspace_id,
|
||||
comment=f"removed from milestone {milestone_name}",
|
||||
old_identifier=(
|
||||
requested_data.get("milestone_id")
|
||||
if requested_data.get("milestone_id") is not None
|
||||
else None
|
||||
requested_data.get("milestone_id") if requested_data.get("milestone_id") is not None else None
|
||||
),
|
||||
epoch=epoch,
|
||||
)
|
||||
@@ -1189,9 +1108,7 @@ def create_link_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1220,9 +1137,7 @@ def update_link_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
if current_instance.get("url") != requested_data.get("url"):
|
||||
issue_activities.append(
|
||||
@@ -1253,9 +1168,7 @@ def delete_link_activity(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1284,9 +1197,7 @@ def create_attachment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1378,9 +1289,7 @@ def delete_issue_reaction_activity(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1422,11 +1331,7 @@ def create_comment_reaction_activity(
|
||||
.first()
|
||||
)
|
||||
comment = IssueComment.objects.get(pk=comment_id, project_id=project_id)
|
||||
if (
|
||||
comment is not None
|
||||
and comment_reaction_id is not None
|
||||
and comment_id is not None
|
||||
):
|
||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=comment.issue_id,
|
||||
@@ -1455,14 +1360,10 @@ def delete_comment_reaction_activity(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_id = (
|
||||
IssueComment.objects.filter(
|
||||
pk=current_instance.get("comment_id"), project_id=project_id
|
||||
)
|
||||
IssueComment.objects.filter(pk=current_instance.get("comment_id"), project_id=project_id)
|
||||
.values_list("issue_id", flat=True)
|
||||
.first()
|
||||
)
|
||||
@@ -1525,9 +1426,7 @@ def delete_issue_vote_activity(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if current_instance and current_instance.get("vote") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1558,9 +1457,7 @@ def create_issue_relation_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if current_instance is None and requested_data.get("issues") is not None:
|
||||
for related_issue in requested_data.get("issues"):
|
||||
issue = Issue.objects.get(pk=related_issue)
|
||||
@@ -1609,9 +1506,7 @@ def delete_issue_relation_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
issue = Issue.objects.get(pk=requested_data.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1689,13 +1584,8 @@ def update_draft_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if (
|
||||
requested_data.get("is_draft") is not None
|
||||
and requested_data.get("is_draft") is False
|
||||
):
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") is False:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
@@ -1756,9 +1646,7 @@ def create_intake_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
status_dict = {
|
||||
-2: "Pending",
|
||||
-1: "Rejected",
|
||||
@@ -1833,11 +1721,7 @@ def create_customer_activity(
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
|
||||
field = (
|
||||
"customer_request"
|
||||
if requested_data.get("customer_request_id") is not None
|
||||
else "customer"
|
||||
)
|
||||
field = "customer_request" if requested_data.get("customer_request_id") is not None else "customer"
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1868,11 +1752,7 @@ def delete_customer_activity(
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
|
||||
field = (
|
||||
"customer_request"
|
||||
if requested_data.get("customer_request_id") is not None
|
||||
else "customer"
|
||||
)
|
||||
field = "customer_request" if requested_data.get("customer_request_id") is not None else "customer"
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -2059,11 +1939,7 @@ def issue_activity(
|
||||
workspace_id = project.workspace_id
|
||||
|
||||
if issue_id is not None:
|
||||
issue = (
|
||||
Issue.objects.filter(pk=issue_id)
|
||||
.filter(Q(type__isnull=True) | Q(type__is_epic=False))
|
||||
.first()
|
||||
)
|
||||
issue = Issue.objects.filter(pk=issue_id).filter(Q(type__isnull=True) | Q(type__is_epic=False)).first()
|
||||
if origin and issue:
|
||||
ri = redis_instance()
|
||||
# set the request origin in redis
|
||||
@@ -2133,26 +2009,18 @@ def issue_activity(
|
||||
if len(issue_activities_created):
|
||||
for activity in issue_activities_created:
|
||||
webhook_activity.delay(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "intake_issue" if intake else "issue"
|
||||
),
|
||||
event=("issue_comment" if activity.field == "comment" else "intake_issue" if intake else "issue"),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
else intake if intake else activity.issue_id
|
||||
else intake
|
||||
if intake
|
||||
else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
"description" if activity.field == "comment" else activity.field
|
||||
),
|
||||
old_value=(
|
||||
activity.old_value if activity.old_value != "" else None
|
||||
),
|
||||
new_value=(
|
||||
activity.new_value if activity.new_value != "" else None
|
||||
),
|
||||
field=("description" if activity.field == "comment" else activity.field),
|
||||
old_value=(activity.old_value if activity.old_value != "" else None),
|
||||
new_value=(activity.new_value if activity.new_value != "" else None),
|
||||
actor_id=activity.actor_id,
|
||||
current_site=origin,
|
||||
slug=activity.workspace.slug,
|
||||
|
||||
@@ -18,7 +18,7 @@ def process_workitem_notifications(
|
||||
requested_data=None,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
notification_type=""
|
||||
notification_type="",
|
||||
):
|
||||
"""
|
||||
Process notifications for issue activities.
|
||||
@@ -39,13 +39,13 @@ def process_workitem_notifications(
|
||||
requested_data=requested_data,
|
||||
current_instance=current_instance,
|
||||
subscriber=subscriber,
|
||||
notification_type=notification_type
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
||||
|
||||
# Process notifications
|
||||
handler = WorkItemNotificationHandler(context)
|
||||
payload = handler.process()
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"in_app_count": len(payload.in_app_notifications),
|
||||
@@ -53,7 +53,4 @@ def process_workitem_notifications(
|
||||
}
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Python imports
|
||||
import json
|
||||
import logging
|
||||
|
||||
# Django imports
|
||||
|
||||
@@ -19,9 +19,7 @@ class CredentialsUpdate(DatabaseMigration):
|
||||
def update_credentials(self):
|
||||
print("Updating credentials...")
|
||||
|
||||
total_count = self._execute_with_error_handling(
|
||||
"SELECT COUNT(*) as count FROM silo.credentials"
|
||||
)[0]["count"]
|
||||
total_count = self._execute_with_error_handling("SELECT COUNT(*) as count FROM silo.credentials")[0]["count"]
|
||||
|
||||
total_batches = (total_count + self.batch_size - 1) // self.batch_size
|
||||
records_processed = 0
|
||||
|
||||
@@ -40,9 +40,7 @@ class DatabaseMigration:
|
||||
def migrate_credentials(self):
|
||||
print("Migrating credentials...")
|
||||
|
||||
total_count = self._execute_with_error_handling(
|
||||
"SELECT COUNT(*) as count FROM silo.credentials"
|
||||
)[0]["count"]
|
||||
total_count = self._execute_with_error_handling("SELECT COUNT(*) as count FROM silo.credentials")[0]["count"]
|
||||
|
||||
total_batches = (total_count + self.batch_size - 1) // self.batch_size
|
||||
records_processed = 0
|
||||
@@ -77,9 +75,7 @@ class DatabaseMigration:
|
||||
)
|
||||
for cred in current_batch
|
||||
]
|
||||
WorkspaceCredential.objects.bulk_create(
|
||||
credentials, ignore_conflicts=True
|
||||
)
|
||||
WorkspaceCredential.objects.bulk_create(credentials, ignore_conflicts=True)
|
||||
|
||||
records_processed += len(current_batch)
|
||||
print(
|
||||
@@ -94,9 +90,9 @@ class DatabaseMigration:
|
||||
def migrate_workspace_connections(self):
|
||||
print("Migrating workspace connections...")
|
||||
|
||||
total_count = self._execute_with_error_handling(
|
||||
"SELECT COUNT(*) as count FROM silo.workspace_connections"
|
||||
)[0]["count"]
|
||||
total_count = self._execute_with_error_handling("SELECT COUNT(*) as count FROM silo.workspace_connections")[0][
|
||||
"count"
|
||||
]
|
||||
|
||||
total_batches = (total_count + self.batch_size - 1) // self.batch_size
|
||||
records_processed = 0
|
||||
@@ -132,9 +128,7 @@ class DatabaseMigration:
|
||||
)
|
||||
for conn in current_batch
|
||||
]
|
||||
WorkspaceConnection.objects.bulk_create(
|
||||
connections, ignore_conflicts=True
|
||||
)
|
||||
WorkspaceConnection.objects.bulk_create(connections, ignore_conflicts=True)
|
||||
|
||||
records_processed += len(current_batch)
|
||||
print(
|
||||
@@ -149,9 +143,9 @@ class DatabaseMigration:
|
||||
def migrate_entity_connections(self):
|
||||
print("Migrating entity connections...")
|
||||
|
||||
total_count = self._execute_with_error_handling(
|
||||
"SELECT COUNT(*) as count FROM silo.entity_connections"
|
||||
)[0]["count"]
|
||||
total_count = self._execute_with_error_handling("SELECT COUNT(*) as count FROM silo.entity_connections")[0][
|
||||
"count"
|
||||
]
|
||||
|
||||
total_batches = (total_count + self.batch_size - 1) // self.batch_size
|
||||
records_processed = 0
|
||||
@@ -176,11 +170,7 @@ class DatabaseMigration:
|
||||
project_id=entity["project_id"],
|
||||
workspace_connection_id=entity["workspace_connection_id"],
|
||||
type=entity["connection_type"],
|
||||
entity_type=(
|
||||
entity["entity_data"]["type"]
|
||||
if "type" in entity["entity_data"]
|
||||
else None
|
||||
),
|
||||
entity_type=(entity["entity_data"]["type"] if "type" in entity["entity_data"] else None),
|
||||
entity_id=entity["entity_id"],
|
||||
entity_slug=entity["entity_slug"],
|
||||
entity_data=entity["entity_data"],
|
||||
@@ -190,9 +180,7 @@ class DatabaseMigration:
|
||||
)
|
||||
for entity in current_batch
|
||||
]
|
||||
WorkspaceEntityConnection.objects.bulk_create(
|
||||
entities, ignore_conflicts=True
|
||||
)
|
||||
WorkspaceEntityConnection.objects.bulk_create(entities, ignore_conflicts=True)
|
||||
|
||||
records_processed += len(current_batch)
|
||||
print(
|
||||
@@ -207,9 +195,7 @@ class DatabaseMigration:
|
||||
def migrate_jobs(self):
|
||||
print("Migrating jobs...")
|
||||
|
||||
total_count = self._execute_with_error_handling(
|
||||
"SELECT COUNT(*) as count FROM silo.job_configs"
|
||||
)[0]["count"]
|
||||
total_count = self._execute_with_error_handling("SELECT COUNT(*) as count FROM silo.job_configs")[0]["count"]
|
||||
|
||||
report_ids = []
|
||||
total_batches = (total_count + self.batch_size - 1) // self.batch_size
|
||||
@@ -286,11 +272,7 @@ class DatabaseMigration:
|
||||
project_id=job["project_id"],
|
||||
workspace_id=job["workspace_id"],
|
||||
initiator_id=job["initiator_id"],
|
||||
report_id=(
|
||||
report_ids[i]
|
||||
if i < len(report_ids)
|
||||
else str(uuid.uuid4())
|
||||
),
|
||||
report_id=(report_ids[i] if i < len(report_ids) else str(uuid.uuid4())),
|
||||
status=job["status"],
|
||||
created_at=job["created_at"],
|
||||
updated_at=job["updated_at"],
|
||||
@@ -304,8 +286,7 @@ class DatabaseMigration:
|
||||
|
||||
records_processed += len(current_batch)
|
||||
print(
|
||||
f"Processed jobs batch {batch_num + 1}/{total_batches} "
|
||||
f"({records_processed}/{total_count} records)"
|
||||
f"Processed jobs batch {batch_num + 1}/{total_batches} ({records_processed}/{total_count} records)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -14,11 +14,10 @@ from typing import Optional
|
||||
from plane.db.models import IssueLink, ModuleLink
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.ee.models import InitiativeLink, ProjectLink
|
||||
from typing import Literal, Type, Union
|
||||
from typing import Literal
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.apps import apps
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
|
||||
|
||||
@@ -107,56 +107,60 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||
)
|
||||
|
||||
# Create project members
|
||||
ProjectMember.objects.bulk_create([
|
||||
ProjectMember(
|
||||
project=project,
|
||||
member_id=workspace_member["member_id"],
|
||||
role=workspace_member["role"],
|
||||
workspace_id=workspace.id,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
for workspace_member in workspace_members
|
||||
])
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
project=project,
|
||||
member_id=workspace_member["member_id"],
|
||||
role=workspace_member["role"],
|
||||
workspace_id=workspace.id,
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
for workspace_member in workspace_members
|
||||
]
|
||||
)
|
||||
|
||||
# Create issue user properties
|
||||
IssueUserProperty.objects.bulk_create([
|
||||
IssueUserProperty(
|
||||
project=project,
|
||||
user_id=workspace_member["member_id"],
|
||||
workspace_id=workspace.id,
|
||||
display_filters={
|
||||
"layout": "list",
|
||||
"calendar": {"layout": "month", "show_weekends": False},
|
||||
"group_by": "state",
|
||||
"order_by": "sort_order",
|
||||
"sub_issue": True,
|
||||
"sub_group_by": None,
|
||||
"show_empty_groups": True,
|
||||
},
|
||||
display_properties={
|
||||
"key": True,
|
||||
"link": True,
|
||||
"cycle": False,
|
||||
"state": True,
|
||||
"labels": False,
|
||||
"modules": False,
|
||||
"assignee": True,
|
||||
"due_date": False,
|
||||
"estimate": True,
|
||||
"priority": True,
|
||||
"created_on": True,
|
||||
"issue_type": True,
|
||||
"start_date": False,
|
||||
"updated_on": True,
|
||||
"customer_count": True,
|
||||
"sub_issue_count": False,
|
||||
"attachment_count": False,
|
||||
"customer_request_count": True,
|
||||
},
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
for workspace_member in workspace_members
|
||||
])
|
||||
IssueUserProperty.objects.bulk_create(
|
||||
[
|
||||
IssueUserProperty(
|
||||
project=project,
|
||||
user_id=workspace_member["member_id"],
|
||||
workspace_id=workspace.id,
|
||||
display_filters={
|
||||
"layout": "list",
|
||||
"calendar": {"layout": "month", "show_weekends": False},
|
||||
"group_by": "state",
|
||||
"order_by": "sort_order",
|
||||
"sub_issue": True,
|
||||
"sub_group_by": None,
|
||||
"show_empty_groups": True,
|
||||
},
|
||||
display_properties={
|
||||
"key": True,
|
||||
"link": True,
|
||||
"cycle": False,
|
||||
"state": True,
|
||||
"labels": False,
|
||||
"modules": False,
|
||||
"assignee": True,
|
||||
"due_date": False,
|
||||
"estimate": True,
|
||||
"priority": True,
|
||||
"created_on": True,
|
||||
"issue_type": True,
|
||||
"start_date": False,
|
||||
"updated_on": True,
|
||||
"customer_count": True,
|
||||
"sub_issue_count": False,
|
||||
"attachment_count": False,
|
||||
"customer_request_count": True,
|
||||
},
|
||||
created_by_id=bot_user.id,
|
||||
)
|
||||
for workspace_member in workspace_members
|
||||
]
|
||||
)
|
||||
# update map
|
||||
projects_map[project_id] = project.id
|
||||
logger.info(f"Task: workspace_seed_task -> Project {project_id} created")
|
||||
|
||||
@@ -27,15 +27,13 @@ def backfill_issue_type_task(projects):
|
||||
# Update the issue type for all existing issues
|
||||
issue_types = {
|
||||
str(issue_type["project_id"]): str(issue_type["id"])
|
||||
for issue_type in IssueType.objects.filter(
|
||||
project_id__in=[project["id"] for project in projects]
|
||||
).values("id", "project_id")
|
||||
for issue_type in IssueType.objects.filter(project_id__in=[project["id"] for project in projects]).values(
|
||||
"id", "project_id"
|
||||
)
|
||||
}
|
||||
# Update the issue type for all existing issues
|
||||
bulk_issues = []
|
||||
for issue in Issue.objects.filter(
|
||||
project_id__in=[project["id"] for project in projects]
|
||||
):
|
||||
for issue in Issue.objects.filter(project_id__in=[project["id"] for project in projects]):
|
||||
issue.type_id = issue_types[str(issue.project_id)]
|
||||
bulk_issues.append(issue)
|
||||
|
||||
|
||||
@@ -49,9 +49,7 @@ class Command(BaseCommand):
|
||||
if project_id == "":
|
||||
raise CommandError("Project ID is required")
|
||||
|
||||
project = Project.objects.filter(
|
||||
id=project_id, workspace__slug=workspace_slug
|
||||
).first()
|
||||
project = Project.objects.filter(id=project_id, workspace__slug=workspace_slug).first()
|
||||
|
||||
if not project:
|
||||
raise CommandError("Project does not exists")
|
||||
@@ -87,9 +85,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
# Get the maximum sequence_id
|
||||
last_id = IssueSequence.objects.filter(project=project).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
last_id = IssueSequence.objects.filter(project=project).aggregate(largest=Max("sequence"))["largest"]
|
||||
|
||||
last_id = 1 if last_id is None else last_id + 1
|
||||
|
||||
@@ -98,9 +94,7 @@ class Command(BaseCommand):
|
||||
project=project, state_id=states[random.randint(0, len(states) - 1)]
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
|
||||
largest_sort_order = (
|
||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
)
|
||||
largest_sort_order = 65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
fake = Faker()
|
||||
Faker.seed(0)
|
||||
issues = []
|
||||
@@ -113,9 +107,7 @@ class Command(BaseCommand):
|
||||
workspace=workspace,
|
||||
name=sentence[:254],
|
||||
sequence_id=last_id,
|
||||
priority=["urgent", "high", "medium", "low", "none"][
|
||||
random.randint(0, 4)
|
||||
],
|
||||
priority=["urgent", "high", "medium", "low", "none"][random.randint(0, 4)],
|
||||
created_by_id=project.created_by_id,
|
||||
)
|
||||
)
|
||||
@@ -123,9 +115,7 @@ class Command(BaseCommand):
|
||||
largest_sort_order = largest_sort_order + random.randint(0, 1000)
|
||||
last_id = last_id + 1
|
||||
|
||||
issues = Issue.objects.bulk_create(
|
||||
issues, ignore_conflicts=True, batch_size=1000
|
||||
)
|
||||
issues = Issue.objects.bulk_create(issues, ignore_conflicts=True, batch_size=1000)
|
||||
# Sequences
|
||||
IssueSequence.objects.bulk_create(
|
||||
[
|
||||
@@ -140,12 +130,7 @@ class Command(BaseCommand):
|
||||
batch_size=100,
|
||||
)
|
||||
CycleIssue.objects.bulk_create(
|
||||
[
|
||||
CycleIssue(
|
||||
issue=issue, cycle=cycle, project=project, workspace=workspace
|
||||
)
|
||||
for issue in issues
|
||||
],
|
||||
[CycleIssue(issue=issue, cycle=cycle, project=project, workspace=workspace) for issue in issues],
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ class Command(BaseCommand):
|
||||
creator = input("Your email: ")
|
||||
|
||||
if creator == "" or not User.objects.filter(email=creator).exists():
|
||||
raise CommandError(
|
||||
"User email is required and should be existing in Database"
|
||||
)
|
||||
raise CommandError("User email is required and should be existing in Database")
|
||||
|
||||
user = User.objects.get(email=creator)
|
||||
|
||||
@@ -37,9 +35,7 @@ class Command(BaseCommand):
|
||||
module_count = int(input("Number of modules to be created: "))
|
||||
|
||||
# Create workspace
|
||||
workspace = Workspace.objects.create(
|
||||
slug=workspace_slug, name=workspace_name, owner=user
|
||||
)
|
||||
workspace = Workspace.objects.create(slug=workspace_slug, name=workspace_name, owner=user)
|
||||
# Create workspace member
|
||||
WorkspaceMember.objects.create(workspace=workspace, role=20, member=user)
|
||||
|
||||
|
||||
@@ -53,38 +53,26 @@ class Command(BaseCommand):
|
||||
except Workspace.DoesNotExist:
|
||||
raise CommandError(f"Workspace '{workspace_slug}' not found")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Found workspace: {workspace.name} ({workspace.slug})"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Found workspace: {workspace.name} ({workspace.slug})"))
|
||||
|
||||
# Get projects based on whether project_id is provided
|
||||
if project_id:
|
||||
# Process only the specified project
|
||||
try:
|
||||
projects = Project.objects.filter(
|
||||
workspace=workspace, id=project_id
|
||||
)
|
||||
projects = Project.objects.filter(workspace=workspace, id=project_id)
|
||||
if not projects.exists():
|
||||
raise CommandError(
|
||||
f"Project with ID '{project_id}' not found in workspace '{workspace_slug}'"
|
||||
)
|
||||
raise CommandError(f"Project with ID '{project_id}' not found in workspace '{workspace_slug}'")
|
||||
self.stdout.write(
|
||||
f"Processing specific project: {projects.first().name} ({projects.first().identifier})"
|
||||
)
|
||||
except Project.DoesNotExist:
|
||||
raise CommandError(
|
||||
f"Project with ID '{project_id}' not found in workspace '{workspace_slug}'"
|
||||
)
|
||||
raise CommandError(f"Project with ID '{project_id}' not found in workspace '{workspace_slug}'")
|
||||
else:
|
||||
# Get all projects in the workspace
|
||||
projects = Project.objects.filter(workspace=workspace).order_by("name")
|
||||
|
||||
if not projects.exists():
|
||||
self.stdout.write(
|
||||
self.style.WARNING("No projects found in this workspace!")
|
||||
)
|
||||
self.stdout.write(self.style.WARNING("No projects found in this workspace!"))
|
||||
return
|
||||
|
||||
self.stdout.write(f"Found {projects.count()} projects in workspace")
|
||||
@@ -95,16 +83,10 @@ class Command(BaseCommand):
|
||||
# Loop through each project
|
||||
for project in projects:
|
||||
self.stdout.write("\n" + "=" * 60)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Processing project: {project.name} ({project.identifier})"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Processing project: {project.name} ({project.identifier})"))
|
||||
|
||||
# Process this project
|
||||
project_updated_count = self.process_project(
|
||||
project, dry_run, auto_confirm
|
||||
)
|
||||
project_updated_count = self.process_project(project, dry_run, auto_confirm)
|
||||
|
||||
if project_updated_count > 0:
|
||||
total_projects_processed += 1
|
||||
@@ -132,9 +114,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
if not duplicate_sequences:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(" ✓ No duplicate sequences found in this project!")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(" ✓ No duplicate sequences found in this project!"))
|
||||
return 0
|
||||
|
||||
self.stdout.write(
|
||||
@@ -148,9 +128,7 @@ class Command(BaseCommand):
|
||||
total_duplicates = 0
|
||||
|
||||
for sequence_id in duplicate_sequences:
|
||||
issues = Issue.objects.filter(
|
||||
project=project, sequence_id=sequence_id
|
||||
).order_by(
|
||||
issues = Issue.objects.filter(project=project, sequence_id=sequence_id).order_by(
|
||||
"created_at"
|
||||
) # Order by creation time, keep the oldest
|
||||
|
||||
@@ -159,20 +137,12 @@ class Command(BaseCommand):
|
||||
|
||||
self.stdout.write(f" Sequence {sequence_id}: {len(issues)} issues")
|
||||
for issue in issues:
|
||||
self.stdout.write(
|
||||
f" - {issue.name} (ID: {issue.id}, Created: {issue.created_at})"
|
||||
)
|
||||
self.stdout.write(f" - {issue.name} (ID: {issue.id}, Created: {issue.created_at})")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f" Total issues to be updated in this project: {total_duplicates}"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.WARNING(f" Total issues to be updated in this project: {total_duplicates}"))
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(" DRY RUN: No changes were made for this project.")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(" DRY RUN: No changes were made for this project."))
|
||||
return 0
|
||||
|
||||
# Ask for confirmation unless auto-confirm is enabled
|
||||
@@ -202,10 +172,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Get the current maximum sequence for the project
|
||||
last_sequence = (
|
||||
IssueSequence.objects.filter(project=project).aggregate(
|
||||
largest=Max("sequence")
|
||||
)["largest"]
|
||||
or 0
|
||||
IssueSequence.objects.filter(project=project).aggregate(largest=Max("sequence"))["largest"] or 0
|
||||
)
|
||||
|
||||
# Prepare bulk updates
|
||||
@@ -214,10 +181,7 @@ class Command(BaseCommand):
|
||||
new_sequence = last_sequence
|
||||
|
||||
# Get all issue sequences for this project for efficient lookup
|
||||
issue_sequence_map = {
|
||||
isq.issue_id: isq
|
||||
for isq in IssueSequence.objects.filter(project=project)
|
||||
}
|
||||
issue_sequence_map = {isq.issue_id: isq for isq in IssueSequence.objects.filter(project=project)}
|
||||
|
||||
updated_count = 0
|
||||
|
||||
@@ -226,9 +190,7 @@ class Command(BaseCommand):
|
||||
# Keep the first issue (oldest), update the rest
|
||||
issues_to_update = issues[1:] # Skip the first one
|
||||
|
||||
self.stdout.write(
|
||||
f" Keeping issue '{issues[0].name}' with sequence {sequence_id}"
|
||||
)
|
||||
self.stdout.write(f" Keeping issue '{issues[0].name}' with sequence {sequence_id}")
|
||||
|
||||
for issue in issues_to_update:
|
||||
new_sequence += 1
|
||||
@@ -243,9 +205,7 @@ class Command(BaseCommand):
|
||||
sequence_obj.sequence = new_sequence
|
||||
bulk_issue_sequences.append(sequence_obj)
|
||||
|
||||
self.stdout.write(
|
||||
f" Updating '{issue.name}': {old_sequence} -> {new_sequence}"
|
||||
)
|
||||
self.stdout.write(f" Updating '{issue.name}': {old_sequence} -> {new_sequence}")
|
||||
|
||||
# Perform bulk updates
|
||||
if bulk_issues:
|
||||
@@ -254,14 +214,10 @@ class Command(BaseCommand):
|
||||
|
||||
if bulk_issue_sequences:
|
||||
IssueSequence.objects.bulk_update(bulk_issue_sequences, ["sequence"])
|
||||
self.stdout.write(
|
||||
f" Updated {len(bulk_issue_sequences)} issue sequences"
|
||||
)
|
||||
self.stdout.write(f" Updated {len(bulk_issue_sequences)} issue sequences")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f" ✓ Successfully fixed duplicate sequences! Updated {updated_count} issues."
|
||||
)
|
||||
self.style.SUCCESS(f" ✓ Successfully fixed duplicate sequences! Updated {updated_count} issues.")
|
||||
)
|
||||
|
||||
return updated_count
|
||||
|
||||
@@ -37,9 +37,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Check if status code is 204
|
||||
if response.status_code == 204:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("License key verified successfully")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("License key verified successfully"))
|
||||
return
|
||||
|
||||
elif response.status_code == 400:
|
||||
@@ -58,9 +56,7 @@ class Command(BaseCommand):
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Instance created successfully")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Instance created successfully"))
|
||||
|
||||
return
|
||||
else:
|
||||
|
||||
@@ -64,11 +64,7 @@ class Command(BaseCommand):
|
||||
|
||||
for field in required_fields:
|
||||
value = getattr(template, field, None)
|
||||
if (
|
||||
value is None
|
||||
or value == ""
|
||||
or (isinstance(value, list) and len(value) == 0)
|
||||
):
|
||||
if value is None or value == "" or (isinstance(value, list) and len(value) == 0):
|
||||
missing_fields.append(field)
|
||||
|
||||
return missing_fields
|
||||
@@ -99,40 +95,24 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
# Ask user if the template should be marked as is_verified
|
||||
is_verified_input = (
|
||||
input("Should the template be marked as is_verified? (y/n): ")
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
is_verified_input = input("Should the template be marked as is_verified? (y/n): ").strip().lower()
|
||||
if is_verified_input == "y":
|
||||
template.is_verified = True
|
||||
elif is_verified_input == "n":
|
||||
template.is_verified = False
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
"Invalid input for is_verified. Please enter 'y' or 'n'."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("Invalid input for is_verified. Please enter 'y' or 'n'."))
|
||||
return
|
||||
|
||||
template.is_published = True
|
||||
template.save()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Template {template_id} published successfully."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Template {template_id} published successfully."))
|
||||
elif action == "unpublish":
|
||||
template.is_published = False
|
||||
template.save()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Template {template_id} unpublished successfully."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Template {template_id} unpublished successfully."))
|
||||
|
||||
except Template.DoesNotExist:
|
||||
raise CommandError(f"Template with id {template_id} does not exist")
|
||||
|
||||
@@ -34,13 +34,11 @@ class PageQuerySet(SoftDeletionQuerySet):
|
||||
projects__project_projectmember__is_active=True,
|
||||
)
|
||||
|
||||
if check_workspace_feature_flag(
|
||||
feature_key=FeatureFlag.TEAMSPACES, user_id=user_id, slug=slug
|
||||
):
|
||||
if check_workspace_feature_flag(feature_key=FeatureFlag.TEAMSPACES, user_id=user_id, slug=slug):
|
||||
## Get all team ids where the user is a member
|
||||
teamspace_ids = TeamspaceMember.objects.filter(
|
||||
member_id=user_id, workspace__slug=slug
|
||||
).values_list("team_space_id", flat=True)
|
||||
teamspace_ids = TeamspaceMember.objects.filter(member_id=user_id, workspace__slug=slug).values_list(
|
||||
"team_space_id", flat=True
|
||||
)
|
||||
|
||||
member_project_ids = ProjectMember.objects.filter(
|
||||
member_id=user_id, workspace__slug=slug, is_active=True
|
||||
@@ -112,7 +110,7 @@ class Page(BaseModel):
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
objects = PageManager()
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page"
|
||||
verbose_name_plural = "Pages"
|
||||
@@ -126,9 +124,7 @@ class Page(BaseModel):
|
||||
deferred_fields = self.get_deferred_fields()
|
||||
self._original_name = self.name if "name" not in deferred_fields else None
|
||||
self._original_description_stripped = (
|
||||
self.description_stripped
|
||||
if "description_stripped" not in deferred_fields
|
||||
else None
|
||||
self.description_stripped if "description_stripped" not in deferred_fields else None
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -149,8 +145,7 @@ class Page(BaseModel):
|
||||
return (
|
||||
self.description_html == "<p></p>"
|
||||
or self.description_html == '<p class="editor-paragraph-block"></p>'
|
||||
or self.description_html
|
||||
== '<p class="editor-paragraph-block"></p><p class="editor-paragraph-block"></p>'
|
||||
or self.description_html == '<p class="editor-paragraph-block"></p><p class="editor-paragraph-block"></p>'
|
||||
or not self.description_html
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from ..mixins import TimeAuditModel
|
||||
from plane.utils.color import get_random_color
|
||||
|
||||
|
||||
|
||||
def get_default_onboarding():
|
||||
return {
|
||||
"profile_complete": False,
|
||||
|
||||
@@ -48,9 +48,7 @@ def delete_automation_activity(
|
||||
automation_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
automation_activities.append(
|
||||
AutomationActivity(
|
||||
automation=automation,
|
||||
@@ -106,9 +104,7 @@ def update_automation_activity(
|
||||
TRACKED_FIELDS = ["name", "description", "status", "scope", "is_enabled"]
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
# Track changes for each field present in the requested data
|
||||
for field_name in requested_data:
|
||||
@@ -194,9 +190,7 @@ def update_automation_node_activity(
|
||||
TRACKED_FIELDS = ["name", "node_type", "is_enabled", "handler_name", "config"]
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
for field_name in requested_data:
|
||||
if field_name in TRACKED_FIELDS:
|
||||
@@ -287,14 +281,10 @@ def track_automation_edge_field_change(
|
||||
old_value=current_instance.get(field_name),
|
||||
new_value=requested_data.get(field_name),
|
||||
old_identifier=(
|
||||
current_instance.get(field_name)
|
||||
if field_name in ["target_node", "source_node"]
|
||||
else None
|
||||
current_instance.get(field_name) if field_name in ["target_node", "source_node"] else None
|
||||
),
|
||||
new_identifier=(
|
||||
requested_data.get(field_name)
|
||||
if field_name in ["target_node", "source_node"]
|
||||
else None
|
||||
requested_data.get(field_name) if field_name in ["target_node", "source_node"] else None
|
||||
),
|
||||
epoch=epoch,
|
||||
)
|
||||
@@ -313,9 +303,7 @@ def update_automation_edge_activity(
|
||||
):
|
||||
TRACKED_FIELDS = ["target_node", "source_node", "execution_order"]
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
for field_name in requested_data:
|
||||
if field_name in TRACKED_FIELDS:
|
||||
|
||||
@@ -59,9 +59,7 @@ def process_batched_opensearch_updates():
|
||||
f"OpenSearch batch processed {chunk_processed} {model_name} instances with registry-powered cascades"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing chunk {chunk_info['chunk_number']} for {model_name}: {e}"
|
||||
)
|
||||
logger.error(f"Error processing chunk {chunk_info['chunk_number']} for {model_name}: {e}")
|
||||
# Continue processing other chunks even if one fails
|
||||
|
||||
total_processed += model_processed
|
||||
|
||||
@@ -26,6 +26,7 @@ from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.webhook_task import webhook_activity
|
||||
from plane.bgtasks.notification_task import process_workitem_notifications
|
||||
|
||||
|
||||
# Track changes in issue labels
|
||||
def track_labels(
|
||||
requested_data,
|
||||
@@ -75,14 +76,10 @@ def track_assignees(
|
||||
epoch,
|
||||
):
|
||||
requested_assignees = (
|
||||
set([str(asg) for asg in requested_data.get("assignee_ids", [])])
|
||||
if requested_data is not None
|
||||
else set()
|
||||
set([str(asg) for asg in requested_data.get("assignee_ids", [])]) if requested_data is not None else set()
|
||||
)
|
||||
current_assignees = (
|
||||
set([str(asg) for asg in current_instance.get("assignee_ids", [])])
|
||||
if current_instance is not None
|
||||
else set()
|
||||
set([str(asg) for asg in current_instance.get("assignee_ids", [])]) if current_instance is not None else set()
|
||||
)
|
||||
|
||||
added_assignees = requested_assignees - current_assignees
|
||||
@@ -117,9 +114,7 @@ def track_assignees(
|
||||
)
|
||||
|
||||
# Create assignees subscribers to the issue and ignore if already
|
||||
IssueSubscriber.objects.bulk_create(
|
||||
bulk_subscribers, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
IssueSubscriber.objects.bulk_create(bulk_subscribers, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
|
||||
def create_cycle_issue_activity(
|
||||
@@ -133,9 +128,7 @@ def create_cycle_issue_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
if requested_data.get("cycle_id") and current_instance.get("cycle_id"):
|
||||
new_cycle = Cycle.objects.filter(pk=requested_data.get("cycle_id")).first()
|
||||
@@ -190,9 +183,7 @@ def delete_cycle_issue_activity(
|
||||
) -> List[IssueActivity]:
|
||||
"""Create a cycle activity when a cycle is removed from an issue"""
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
# If the issue has a cycle, create a cycle activity
|
||||
if current_instance.get("cycle_id"):
|
||||
@@ -229,9 +220,7 @@ def update_issue_activity(
|
||||
ISSUE_ACTIVITY_MAPPER = {"label_ids": track_labels, "assignee_ids": track_assignees}
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key)
|
||||
@@ -307,13 +296,7 @@ def bulk_issue_activity(
|
||||
if len(issue_activities_created):
|
||||
for activity in issue_activities_created:
|
||||
webhook_activity.delay(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "inbox_issue"
|
||||
if inbox
|
||||
else "issue"
|
||||
),
|
||||
event=("issue_comment" if activity.field == "comment" else "inbox_issue" if inbox else "issue"),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
@@ -322,15 +305,9 @@ def bulk_issue_activity(
|
||||
else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
"description" if activity.field == "comment" else activity.field
|
||||
),
|
||||
old_value=(
|
||||
activity.old_value if activity.old_value != "" else None
|
||||
),
|
||||
new_value=(
|
||||
activity.new_value if activity.new_value != "" else None
|
||||
),
|
||||
field=("description" if activity.field == "comment" else activity.field),
|
||||
old_value=(activity.old_value if activity.old_value != "" else None),
|
||||
new_value=(activity.new_value if activity.new_value != "" else None),
|
||||
actor_id=activity.actor_id,
|
||||
current_site=origin,
|
||||
slug=activity.workspace.slug,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import pytz
|
||||
|
||||
# Python imports
|
||||
@@ -11,20 +10,11 @@ from django.db.models import Subquery
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
from django.db.models import Count, Q, When, Case, Value, FloatField, Sum, F
|
||||
from django.db.models.functions import Cast, Concat
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from plane.ee.models import CycleSettings, ProjectFeature, AutomatedCycleLog
|
||||
from plane.db.models import Cycle, Project, Issue, CycleIssue, Workspace
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.ee.bgtasks.entity_issue_state_progress_task import (
|
||||
entity_issue_state_activity_task,
|
||||
)
|
||||
from plane.utils.host import base_host
|
||||
from plane.db.models import Cycle, Project, Workspace
|
||||
from plane.db.models import BotTypeEnum, ProjectMember
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.cycle_transfer_issues import transfer_cycle_issues
|
||||
|
||||
@@ -40,8 +40,7 @@ def entity_issue_state_activity_task(issue_cycle_data, user_id, slug, action):
|
||||
issue.estimate_point.value
|
||||
if issue.estimate_point
|
||||
and issue.estimate_point.estimate
|
||||
and getattr(issue.estimate_point.estimate, "type", None)
|
||||
in ["points", "time"]
|
||||
and getattr(issue.estimate_point.estimate, "type", None) in ["points", "time"]
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -62,9 +61,7 @@ def entity_issue_state_activity_task(issue_cycle_data, user_id, slug, action):
|
||||
)
|
||||
|
||||
if activity_records:
|
||||
EntityIssueStateActivity.objects.bulk_create(
|
||||
activity_records, batch_size=10
|
||||
)
|
||||
EntityIssueStateActivity.objects.bulk_create(activity_records, batch_size=10)
|
||||
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -26,7 +26,6 @@ from plane.db.models import (
|
||||
Page,
|
||||
PageLog,
|
||||
Issue,
|
||||
WorkspaceMember,
|
||||
ProjectPage,
|
||||
User,
|
||||
)
|
||||
|
||||
@@ -142,11 +142,7 @@ def handle_multi_properties(
|
||||
return
|
||||
|
||||
# Case 2: If the existing value is not empty and the requested value is not empty
|
||||
if (
|
||||
existing_value
|
||||
and requested_value
|
||||
and existing_value[0] != requested_value[0]
|
||||
):
|
||||
if existing_value and requested_value and existing_value[0] != requested_value[0]:
|
||||
bulk_property_activity.append(
|
||||
IssuePropertyActivity(
|
||||
workspace_id=property.workspace_id,
|
||||
@@ -367,9 +363,7 @@ def track_property_file(
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_property_activity(
|
||||
existing_values, requested_values, issue_id, user_id, epoch
|
||||
):
|
||||
def issue_property_activity(existing_values, requested_values, issue_id, user_id, epoch):
|
||||
"""
|
||||
This function is used to create an activity for the issue property changes.
|
||||
"""
|
||||
|
||||
@@ -99,4 +99,4 @@ def process_initiative_notifications(
|
||||
}
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return {"success": False, "error": str(e)}
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@@ -21,7 +21,6 @@ from plane.db.models import (
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.ee.bgtasks.move_page import move_page
|
||||
from plane.bgtasks.copy_s3_object import copy_s3_objects_of_description_and_assets
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.ee.utils.page_descendants import get_descendant_page_ids
|
||||
|
||||
@@ -50,14 +50,8 @@ def track_description(
|
||||
project_activities,
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
):
|
||||
last_activity = (
|
||||
WorkspaceActivity.objects.filter(project_id=project_id)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if current_instance.get("description_html") != requested_data.get("description_html"):
|
||||
last_activity = WorkspaceActivity.objects.filter(project_id=project_id).order_by("-created_at").first()
|
||||
if (
|
||||
last_activity is not None
|
||||
and last_activity.field == "description_html"
|
||||
@@ -158,15 +152,9 @@ def track_target_date(
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
current_instance.get("target_date")
|
||||
if current_instance.get("target_date") is not None
|
||||
else ""
|
||||
),
|
||||
new_value=(
|
||||
requested_data.get("target_date")
|
||||
if requested_data.get("target_date") is not None
|
||||
else ""
|
||||
current_instance.get("target_date") if current_instance.get("target_date") is not None else ""
|
||||
),
|
||||
new_value=(requested_data.get("target_date") if requested_data.get("target_date") is not None else ""),
|
||||
field="target_date",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
@@ -192,15 +180,9 @@ def track_start_date(
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
current_instance.get("start_date")
|
||||
if current_instance.get("start_date") is not None
|
||||
else ""
|
||||
),
|
||||
new_value=(
|
||||
requested_data.get("start_date")
|
||||
if requested_data.get("start_date") is not None
|
||||
else ""
|
||||
current_instance.get("start_date") if current_instance.get("start_date") is not None else ""
|
||||
),
|
||||
new_value=(requested_data.get("start_date") if requested_data.get("start_date") is not None else ""),
|
||||
field="start_date",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
@@ -384,9 +366,7 @@ def track_deploy_board(
|
||||
WorkspaceActivity(
|
||||
actor_id=actor_id,
|
||||
workspace_id=workspace_id,
|
||||
verb="published"
|
||||
if requested_data.get("deploy_board")
|
||||
else "unpublished",
|
||||
verb="published" if requested_data.get("deploy_board") else "unpublished",
|
||||
old_value=current_instance.get("deploy_board"),
|
||||
new_value=requested_data.get("deploy_board"),
|
||||
field="deploy_board",
|
||||
@@ -421,9 +401,7 @@ def track_members(
|
||||
workspace_id=workspace_id,
|
||||
old_identifier=None,
|
||||
new_identifier=member.id,
|
||||
comment="joined the project"
|
||||
if requested_data.get("joined", None)
|
||||
else "added the member",
|
||||
comment="joined the project" if requested_data.get("joined", None) else "added the member",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
@@ -443,9 +421,7 @@ def track_members(
|
||||
workspace_id=workspace_id,
|
||||
old_identifier=member.id,
|
||||
new_identifier=None,
|
||||
comment="removed the members"
|
||||
if current_instance.get("removed")
|
||||
else "left the project",
|
||||
comment="removed the members" if current_instance.get("removed") else "left the project",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
@@ -604,9 +580,7 @@ def update_project_activity(
|
||||
]
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
for key in requested_data:
|
||||
func = project_activity_MAPPER.get(key)
|
||||
@@ -666,9 +640,7 @@ def create_comment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
@@ -695,9 +667,7 @@ def update_comment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
if current_instance.get("comment_html") != requested_data.get("comment_html"):
|
||||
project_activities.append(
|
||||
@@ -750,9 +720,7 @@ def create_link_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
@@ -779,9 +747,7 @@ def update_link_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
if current_instance.get("url") != requested_data.get("url"):
|
||||
project_activities.append(
|
||||
@@ -810,9 +776,7 @@ def delete_link_activity(
|
||||
project_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
@@ -839,9 +803,7 @@ def create_attachment_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
@@ -927,9 +889,7 @@ def delete_project_reaction_activity(
|
||||
project_activities,
|
||||
epoch,
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
@@ -969,11 +929,7 @@ def create_comment_reaction_activity(
|
||||
.first()
|
||||
)
|
||||
comment = IssueComment.objects.get(pk=comment_id, project_id=project_id)
|
||||
if (
|
||||
comment is not None
|
||||
and comment_reaction_id is not None
|
||||
and comment_id is not None
|
||||
):
|
||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
||||
project_activities.append(
|
||||
WorkspaceActivity(
|
||||
actor_id=actor_id,
|
||||
|
||||
@@ -34,9 +34,7 @@ def create_recurring_workitem_activity(
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
recurring_work_item_activities.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
@@ -167,7 +165,6 @@ def track_priority(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
if requested_data.get("priority") != current_instance.get("priority"):
|
||||
recurring_work_item_activities.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
@@ -194,7 +191,6 @@ def track_state(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
if requested_data.get("state").get("id") != current_instance.get("state").get("id"):
|
||||
recurring_work_item_activities.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
@@ -293,9 +289,7 @@ def track_labels(
|
||||
):
|
||||
# Get label IDs from both requested and current data
|
||||
requested_labels = (
|
||||
set([str(lbl.get("id")) for lbl in requested_data.get("labels", [])])
|
||||
if requested_data is not None
|
||||
else set()
|
||||
set([str(lbl.get("id")) for lbl in requested_data.get("labels", [])]) if requested_data is not None else set()
|
||||
)
|
||||
current_labels = (
|
||||
set([str(lbl.get("id")) for lbl in current_instance.get("labels", [])])
|
||||
@@ -361,9 +355,7 @@ def track_modules(
|
||||
):
|
||||
# Get label IDs from both requested and current data
|
||||
requested_modules = (
|
||||
set([str(lbl.get("id")) for lbl in requested_data.get("modules", [])])
|
||||
if requested_data is not None
|
||||
else set()
|
||||
set([str(lbl.get("id")) for lbl in requested_data.get("modules", [])]) if requested_data is not None else set()
|
||||
)
|
||||
current_modules = (
|
||||
set([str(lbl.get("id")) for lbl in current_instance.get("modules", [])])
|
||||
@@ -427,26 +419,11 @@ def track_type(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
if requested_data.get("type").get("id") != current_instance.get("type").get("id"):
|
||||
new_type_id = (
|
||||
requested_data.get("type").get("id") if requested_data.get("type") else None
|
||||
)
|
||||
old_type_id = (
|
||||
current_instance.get("type").get("id")
|
||||
if current_instance.get("type")
|
||||
else None
|
||||
)
|
||||
new_type_name = (
|
||||
requested_data.get("type").get("name")
|
||||
if requested_data.get("type")
|
||||
else None
|
||||
)
|
||||
old_type_name = (
|
||||
current_instance.get("type").get("name")
|
||||
if current_instance.get("type")
|
||||
else None
|
||||
)
|
||||
new_type_id = requested_data.get("type").get("id") if requested_data.get("type") else None
|
||||
old_type_id = current_instance.get("type").get("id") if current_instance.get("type") else None
|
||||
new_type_name = requested_data.get("type").get("name") if requested_data.get("type") else None
|
||||
old_type_name = current_instance.get("type").get("name") if current_instance.get("type") else None
|
||||
recurring_work_item_activities.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
project_id=project_id,
|
||||
@@ -501,9 +478,7 @@ def track_description_html(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get("description_html"):
|
||||
recurring_work_item_activities.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
recurring_workitem_task_id=recurring_workitem_task_id,
|
||||
@@ -529,7 +504,6 @@ def track_properties(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
# Handle both dictionary and list formats for properties
|
||||
requested_properties_data = requested_data.get("properties", [])
|
||||
current_properties_data = current_instance.get("properties", [])
|
||||
@@ -538,16 +512,12 @@ def track_properties(
|
||||
# Convert properties to the expected dictionary format
|
||||
if isinstance(requested_properties_data, list):
|
||||
requested_values = {
|
||||
str(prop.get("id")): prop.get("values", [])
|
||||
for prop in requested_properties_data
|
||||
if prop.get("id")
|
||||
str(prop.get("id")): prop.get("values", []) for prop in requested_properties_data if prop.get("id")
|
||||
}
|
||||
|
||||
if isinstance(current_properties_data, list):
|
||||
current_values = {
|
||||
str(prop.get("id")): prop.get("values", [])
|
||||
for prop in current_properties_data
|
||||
if prop.get("id")
|
||||
str(prop.get("id")): prop.get("values", []) for prop in current_properties_data if prop.get("id")
|
||||
}
|
||||
|
||||
# Log the activity
|
||||
@@ -595,11 +565,8 @@ def update_recurring_workitem_activity(
|
||||
recurring_work_item_activities,
|
||||
epoch,
|
||||
):
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
current_instance = json.loads(current_instance) if current_instance is not None else None
|
||||
|
||||
RECURRING_WORKITEM_TEMPLATE_ACTIVITY_MAPPER = {
|
||||
"name": track_name,
|
||||
@@ -621,9 +588,7 @@ def update_recurring_workitem_activity(
|
||||
}
|
||||
|
||||
if requested_data.get("workitem_blueprint") is not None:
|
||||
|
||||
for key in requested_data.get("workitem_blueprint", {}).keys():
|
||||
|
||||
func = RECURRING_WORKITEM_TEMPLATE_ACTIVITY_MAPPER.get(key)
|
||||
if func is not None:
|
||||
func(
|
||||
@@ -689,9 +654,7 @@ def recurring_work_item_activity(
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
RecurringWorkItemTaskActivity.objects.bulk_create(
|
||||
recurring_work_item_activities
|
||||
)
|
||||
RecurringWorkItemTaskActivity.objects.bulk_create(recurring_work_item_activities)
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
@@ -146,11 +146,7 @@ def handle_multi_properties(
|
||||
return
|
||||
|
||||
# Case 2: If the existing value is not empty and the requested value is not empty
|
||||
if (
|
||||
existing_value
|
||||
and requested_value
|
||||
and existing_value[0] != requested_value[0]
|
||||
):
|
||||
if existing_value and requested_value and existing_value[0] != requested_value[0]:
|
||||
bulk_property_activity.append(
|
||||
RecurringWorkItemTaskActivity(
|
||||
workspace_id=property.workspace_id,
|
||||
@@ -380,9 +376,9 @@ def recurring_work_item_property_activity(
|
||||
"""
|
||||
|
||||
# Get the issue
|
||||
recurring_workitem_task = RecurringWorkitemTask.objects.select_related(
|
||||
"workitem_blueprint"
|
||||
).get(id=recurring_workitem_task_id)
|
||||
recurring_workitem_task = RecurringWorkitemTask.objects.select_related("workitem_blueprint").get(
|
||||
id=recurring_workitem_task_id
|
||||
)
|
||||
issue_type_id = recurring_workitem_task.workitem_blueprint.type.get("id")
|
||||
|
||||
if not issue_type_id:
|
||||
@@ -431,11 +427,9 @@ def recurring_work_item_property_activity(
|
||||
epoch=epoch,
|
||||
)
|
||||
|
||||
|
||||
# Create the bulk activity
|
||||
RecurringWorkItemTaskActivity.objects.bulk_create(bulk_property_activity)
|
||||
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -45,14 +45,14 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
|
||||
try:
|
||||
# Get the recurring task
|
||||
recurring_task = RecurringWorkitemTask.objects.select_related(
|
||||
"workitem_blueprint", "project", "workspace"
|
||||
).filter(id=recurring_workitem_task_id).first()
|
||||
recurring_task = (
|
||||
RecurringWorkitemTask.objects.select_related("workitem_blueprint", "project", "workspace")
|
||||
.filter(id=recurring_workitem_task_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not recurring_task:
|
||||
logger.info(
|
||||
f"Recurring task {recurring_workitem_task_id} not found, skipping execution"
|
||||
)
|
||||
logger.info(f"Recurring task {recurring_workitem_task_id} not found, skipping execution")
|
||||
return {"status": "skipped", "message": "Task not found"}
|
||||
|
||||
slug = recurring_task.workspace.slug
|
||||
@@ -64,16 +64,12 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
|
||||
# Check if task is enabled
|
||||
if not recurring_task.enabled:
|
||||
logger.info(
|
||||
f"Recurring task {recurring_workitem_task_id} is disabled, skipping execution"
|
||||
)
|
||||
logger.info(f"Recurring task {recurring_workitem_task_id} is disabled, skipping execution")
|
||||
return {"status": "skipped", "message": "Task is disabled"}
|
||||
|
||||
# Check if task has expired
|
||||
if recurring_task.end_at and timezone.now() > recurring_task.end_at:
|
||||
logger.info(
|
||||
f"Recurring task {recurring_workitem_task_id} has expired, skipping execution"
|
||||
)
|
||||
logger.info(f"Recurring task {recurring_workitem_task_id} has expired, skipping execution")
|
||||
return {"status": "skipped", "message": "Task has expired"}
|
||||
|
||||
# Create log entry
|
||||
@@ -100,9 +96,7 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
# check for the workflows for the issue creation
|
||||
# if the state is changed later
|
||||
if workitem_blueprint_first.state.get("id"):
|
||||
workflow_state_manager = WorkflowStateManager(
|
||||
project_id=project_id, slug=slug
|
||||
)
|
||||
workflow_state_manager = WorkflowStateManager(project_id=project_id, slug=slug)
|
||||
if workflow_state_manager.validate_issue_creation(
|
||||
state_id=workitem_blueprint_first.state.get("id"), user_id=user_id
|
||||
):
|
||||
@@ -111,17 +105,13 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
# get all the states in the project and create a mapping dict
|
||||
state_map = {
|
||||
str(state.id): str(state.id)
|
||||
for state in State.objects.filter(
|
||||
project_id=project_id, workspace_id=workspace_id
|
||||
)
|
||||
for state in State.objects.filter(project_id=project_id, workspace_id=workspace_id)
|
||||
}
|
||||
|
||||
# get the labels in the project and create a mapping dict
|
||||
label_map = {
|
||||
str(label.id): str(label.id)
|
||||
for label in Label.objects.filter(
|
||||
project_id=project_id, workspace_id=workspace_id
|
||||
)
|
||||
for label in Label.objects.filter(project_id=project_id, workspace_id=workspace_id)
|
||||
}
|
||||
|
||||
# get all the workitem types in the project and create a mapping dict
|
||||
@@ -137,25 +127,19 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
# get all the workitem properties in the project and create a mapping dict
|
||||
workitem_property_map = {
|
||||
str(prop.id): str(prop.id)
|
||||
for prop in IssueProperty.objects.filter(
|
||||
project_id=project_id, workspace_id=workspace_id
|
||||
)
|
||||
for prop in IssueProperty.objects.filter(project_id=project_id, workspace_id=workspace_id)
|
||||
}
|
||||
|
||||
# get all the workitem property options in the project and create a mapping dict
|
||||
workitem_property_option_map = {
|
||||
str(option.id): str(option.id)
|
||||
for option in IssuePropertyOption.objects.filter(
|
||||
project_id=project_id, workspace_id=workspace_id
|
||||
)
|
||||
for option in IssuePropertyOption.objects.filter(project_id=project_id, workspace_id=workspace_id)
|
||||
}
|
||||
|
||||
# get all the estimates in the project and create a mapping dict
|
||||
estimate_point_map = {
|
||||
str(estimate.id): str(estimate.id)
|
||||
for estimate in EstimatePoint.objects.filter(
|
||||
project_id=project_id, workspace_id=workspace_id
|
||||
)
|
||||
for estimate in EstimatePoint.objects.filter(project_id=project_id, workspace_id=workspace_id)
|
||||
}
|
||||
|
||||
work_item = create_workitems(
|
||||
@@ -196,9 +180,7 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
updated_by_id=user_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Successfully created work item {work_item.id} from recurring task {recurring_workitem_task_id}"
|
||||
)
|
||||
logger.info(f"Successfully created work item {work_item.id} from recurring task {recurring_workitem_task_id}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -208,9 +190,7 @@ def create_work_item_from_template(self, recurring_workitem_task_id: str):
|
||||
}
|
||||
|
||||
except RecurringWorkitemTask.DoesNotExist:
|
||||
error_msg = (
|
||||
f"RecurringWorkitemTask with ID {recurring_workitem_task_id} not found"
|
||||
)
|
||||
error_msg = f"RecurringWorkitemTask with ID {recurring_workitem_task_id} not found"
|
||||
logger.error(error_msg)
|
||||
return {"status": "error", "message": error_msg}
|
||||
|
||||
|
||||
@@ -101,12 +101,8 @@ def _get_property_value_data(
|
||||
property_type_handlers = {
|
||||
PropertyTypeEnum.TEXT: lambda v: {"value_text": str(v)},
|
||||
PropertyTypeEnum.BOOLEAN: lambda v: {"value_boolean": bool(v)},
|
||||
PropertyTypeEnum.DECIMAL: lambda v: {
|
||||
"value_decimal": float(v) if v is not None else 0.0
|
||||
},
|
||||
PropertyTypeEnum.DATETIME: lambda v: {
|
||||
"value_datetime": _parse_datetime_value(v)
|
||||
},
|
||||
PropertyTypeEnum.DECIMAL: lambda v: {"value_decimal": float(v) if v is not None else 0.0},
|
||||
PropertyTypeEnum.DATETIME: lambda v: {"value_datetime": _parse_datetime_value(v)},
|
||||
PropertyTypeEnum.URL: lambda v: {"value_text": str(v)},
|
||||
PropertyTypeEnum.EMAIL: lambda v: {"value_text": str(v)},
|
||||
PropertyTypeEnum.RELATION: lambda v: {"value_uuid": uuid.UUID(str(v))},
|
||||
@@ -146,9 +142,7 @@ def _get_property_value_data(
|
||||
return {"value_text": str(value)}
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(
|
||||
f"Failed to process value {value} for property {property_obj.id}: {e}"
|
||||
)
|
||||
logger.warning(f"Failed to process value {value} for property {property_obj.id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -322,9 +316,7 @@ def create_workitem_types(workitem_type_data, project_id, workspace_id, user_id)
|
||||
is_multi=property.get("is_multi", False),
|
||||
)
|
||||
created_issue_property.save(created_by_id=user_id)
|
||||
workitem_property_map[str(property.get("id"))] = str(
|
||||
created_issue_property.id
|
||||
)
|
||||
workitem_property_map[str(property.get("id"))] = str(created_issue_property.id)
|
||||
logger.info(f"Issue property created: {created_issue_property.id}")
|
||||
# create options
|
||||
if created_issue_property.property_type == PropertyTypeEnum.OPTION:
|
||||
@@ -340,12 +332,8 @@ def create_workitem_types(workitem_type_data, project_id, workspace_id, user_id)
|
||||
project_id=project_id,
|
||||
)
|
||||
created_issue_property_option.save(created_by_id=user_id)
|
||||
workitem_property_option_map[str(value.get("id"))] = str(
|
||||
created_issue_property_option.id
|
||||
)
|
||||
logger.info(
|
||||
f"Issue property option created: {created_issue_property_option.id}"
|
||||
)
|
||||
workitem_property_option_map[str(value.get("id"))] = str(created_issue_property_option.id)
|
||||
logger.info(f"Issue property option created: {created_issue_property_option.id}")
|
||||
return workitem_type_map, workitem_property_map, workitem_property_option_map
|
||||
|
||||
|
||||
@@ -370,9 +358,7 @@ def create_epics(epic_data, project_id, workspace_id, user_id):
|
||||
)
|
||||
issue_type.save(created_by_id=user_id)
|
||||
logger.info(f"Epic created: {issue_type.id}")
|
||||
created_project_issue_type = ProjectIssueType(
|
||||
project_id=project_id, issue_type=issue_type, level=1
|
||||
)
|
||||
created_project_issue_type = ProjectIssueType(project_id=project_id, issue_type=issue_type, level=1)
|
||||
created_project_issue_type.save(created_by_id=user_id)
|
||||
logger.info(f"Epic project issue type created: {created_project_issue_type.id}")
|
||||
for property in epic_data.get("properties", []):
|
||||
@@ -487,9 +473,7 @@ def create_issue_property_values(
|
||||
|
||||
# Log the activity
|
||||
issue_property_activity.delay(
|
||||
existing_values={
|
||||
str(prop["property_id"]): prop["values"] for prop in existing_prop_values
|
||||
},
|
||||
existing_values={str(prop["property_id"]): prop["values"] for prop in existing_prop_values},
|
||||
requested_values=converted_properties,
|
||||
issue_id=issue.id,
|
||||
user_id=user_id,
|
||||
@@ -530,7 +514,6 @@ def create_workitems(
|
||||
created_workitems = []
|
||||
# Create workitems
|
||||
for blueprint in workitem_blueprints:
|
||||
|
||||
# Check if the state is present in the state map
|
||||
if blueprint.state and state_map:
|
||||
state_id = state_map.get(str(blueprint.state.get("id")))
|
||||
@@ -593,9 +576,7 @@ def create_workitems(
|
||||
created_by_id=user_id,
|
||||
)
|
||||
logger.info(f"Issue activity created: {new_issue_id}")
|
||||
requested_data = json.dumps(
|
||||
IssueDetailSerializer(new_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
requested_data = json.dumps(IssueDetailSerializer(new_issue).data, cls=DjangoJSONEncoder)
|
||||
slug = Workspace.objects.get(id=workspace_id).slug
|
||||
|
||||
# trigger the webhook
|
||||
@@ -616,9 +597,7 @@ def create_workitems(
|
||||
user_id=user_id,
|
||||
is_creating=True,
|
||||
)
|
||||
logger.info(
|
||||
f"triggered the workitem description version for the workitem: {new_issue_id}"
|
||||
)
|
||||
logger.info(f"triggered the workitem description version for the workitem: {new_issue_id}")
|
||||
|
||||
# Create the assignees
|
||||
IssueAssignee.objects.bulk_create(
|
||||
@@ -633,9 +612,7 @@ def create_workitems(
|
||||
for member_id in ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
member_id__in=[
|
||||
str(assignee.get("id")) for assignee in blueprint.assignees
|
||||
],
|
||||
member_id__in=[str(assignee.get("id")) for assignee in blueprint.assignees],
|
||||
).values_list("member_id", flat=True)
|
||||
],
|
||||
batch_size=BULK_CREATE_BATCH_SIZE,
|
||||
@@ -681,9 +658,7 @@ def create_workitems(
|
||||
|
||||
|
||||
@shared_task
|
||||
def create_project_from_template(
|
||||
template_id, project_id, user_id, state_map, origin=None
|
||||
):
|
||||
def create_project_from_template(template_id, project_id, user_id, state_map, origin=None):
|
||||
try:
|
||||
"""
|
||||
Create a project from a template and copy the workitems, labels, estimates, etc.
|
||||
@@ -707,22 +682,16 @@ def create_project_from_template(
|
||||
|
||||
# create estimates
|
||||
if project_template.estimates:
|
||||
estimate_point_map = create_estimates(
|
||||
project_template.estimates, project_id, project, user_id
|
||||
)
|
||||
estimate_point_map = create_estimates(project_template.estimates, project_id, project, user_id)
|
||||
|
||||
# create labels
|
||||
if project_template.labels:
|
||||
label_map = create_labels(
|
||||
project_template.labels, project_id, workspace_id, user_id
|
||||
)
|
||||
label_map = create_labels(project_template.labels, project_id, workspace_id, user_id)
|
||||
|
||||
# create workitem types
|
||||
if project_template.workitem_types:
|
||||
workitem_type_map, workitem_property_map, workitem_property_option_map = (
|
||||
create_workitem_types(
|
||||
project_template.workitem_types, project_id, workspace_id, user_id
|
||||
)
|
||||
workitem_type_map, workitem_property_map, workitem_property_option_map = create_workitem_types(
|
||||
project_template.workitem_types, project_id, workspace_id, user_id
|
||||
)
|
||||
|
||||
# create epics
|
||||
@@ -884,9 +853,7 @@ def create_subworkitems(workitem_template_id, project_id, workitem_id, user_id):
|
||||
"created_by_id": user_id,
|
||||
}
|
||||
)
|
||||
property_values_to_create.append(
|
||||
IssuePropertyValue(**property_value_data)
|
||||
)
|
||||
property_values_to_create.append(IssuePropertyValue(**property_value_data))
|
||||
|
||||
if property_values_to_create:
|
||||
IssuePropertyValue.objects.bulk_create(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user