* feat: implement cover image handling and static image selection - Added functionality to handle cover image uploads and selections in project and profile forms. - Introduced a new helper for managing cover images, including static images and uploaded assets. - Updated components to utilize the new cover image helper for displaying and processing cover images. - Added a set of static cover images for selection in the image picker. - Enhanced error handling for image uploads and processing. This update improves the user experience by providing a more robust cover image management system. * refactor: rename STATIC_COVER_IMAGES_ARRAY to STATIC_COVER_IMAGES for consistency - Updated the cover image helper to export STATIC_COVER_IMAGES instead of STATIC_COVER_IMAGES_ARRAY. - Adjusted the ImagePickerPopover component to utilize the renamed export for rendering static cover images. * feat: enhance project creation and image handling - Introduced default project form values with a random emoji for logo props. - Updated cover image handling in various components, ensuring consistent usage of the new cover image helper. - Refactored image picker to improve search functionality and loading states. - Removed unused constants and streamlined cover image type checks for better clarity and performance. This update enhances the user experience in project creation and image selection processes. * refactor: simplify cover image type definition and clean up code - Removed duplicate type from TCoverImageType, streamlining the definition. - Cleaned up whitespace in the cover image helper for improved readability. This update enhances code clarity and maintains consistency in cover image handling. * refactor: update cover image type definitions and simplify logic - Changed ICoverImageResult and ICoverImagePayload interfaces to type aliases for better clarity. - Simplified the logic in getCoverImageDisplayURL function to enhance readability and maintainability. This update improves the structure and clarity of the cover image helper code. * refactor: remove unused project cover image endpoint and update cover image handling - Removed the ProjectPublicCoverImagesEndpoint and its associated URL from the project. - Updated the cover image handling in the cover-image helper to utilize imported assets instead of static paths. - Cleaned up the ProjectFavoritesViewSet and FileService by removing the now obsolete getProjectCoverImages method. This update streamlines the cover image management and eliminates unnecessary code, enhancing overall maintainability. * refactor: update cover image imports to new asset structure - Replaced static path imports for cover images with updated paths to the new asset structure. - This change improves organization and maintainability of cover image assets in the project. This update aligns with recent refactoring efforts to streamline cover image handling. * feat: add additional cover images to the helper - Imported new cover images (24 to 29) into the cover-image helper. - This update expands the available cover image options for use in the project, enhancing visual variety. * refactor: remove ProjectPublicCoverImagesEndpoint from project URLs and views * refactor: update cover image imports to include URL query parameter - Modified cover image imports in the cover-image helper to append a URL query parameter for better asset handling. - This change enhances the way cover images are processed and utilized within the project. * refactor: extract default project form values into a utility function - Created a new utility function `getProjectFormValues` to encapsulate the default project form values. - Updated the `CreateProjectForm` component to use this utility function for setting default form values, improving code organization and maintainability. * feat: integrate project update functionality in CreateProjectForm - Added `updateProject` method to the `CreateProjectForm` component for updating project cover images after creation. - Enhanced cover image handling by ensuring the correct URL is set for both uploaded and existing cover images. This update improves the project creation workflow and ensures cover images are accurately updated. * fix: update documentation for cover image handling - Corrected the comment regarding local static images to reflect that they are served from the assets folder instead of the public folder. - This change ensures accurate documentation for the `getCoverImageType` and `getCoverImageDisplayURL` functions, improving clarity for future developers. * feat: implement random cover image selection for project forms - Replaced the default cover image URL with a new utility function `getRandomCoverImage` that selects a random cover image from the available options. - Updated the `getProjectFormValues` function to utilize this new method, enhancing the project creation experience with varied cover images. --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
570 lines
21 KiB
Python
570 lines
21 KiB
Python
# Python imports
|
|
import json
|
|
|
|
import boto3
|
|
|
|
# Django imports
|
|
from django.conf import settings
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
from django.db.models import Exists, F, OuterRef, Prefetch, Q, 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
|
|
from plane.app.permissions import ROLE, ProjectMemberPermission, allow_permission
|
|
from plane.app.serializers import (
|
|
DeployBoardSerializer,
|
|
ProjectListSerializer,
|
|
ProjectSerializer,
|
|
)
|
|
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,
|
|
Project,
|
|
ProjectIdentifier,
|
|
ProjectMember,
|
|
ProjectNetwork,
|
|
State,
|
|
DEFAULT_STATES,
|
|
Workspace,
|
|
WorkspaceMember,
|
|
)
|
|
from plane.utils.cache import cache_response
|
|
from plane.utils.exception_logger import log_exception
|
|
from plane.utils.host import base_host
|
|
|
|
|
|
class ProjectViewSet(BaseViewSet):
|
|
serializer_class = ProjectListSerializer
|
|
model = Project
|
|
webhook_event = "project"
|
|
use_read_replica = True
|
|
|
|
def get_queryset(self):
|
|
sort_order = ProjectMember.objects.filter(
|
|
member=self.request.user,
|
|
project_id=OuterRef("pk"),
|
|
workspace__slug=self.kwargs.get("slug"),
|
|
is_active=True,
|
|
).values("sort_order")
|
|
return self.filter_queryset(
|
|
super()
|
|
.get_queryset()
|
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
|
.annotate(
|
|
is_favorite=Exists(
|
|
UserFavorite.objects.filter(
|
|
user=self.request.user,
|
|
entity_identifier=OuterRef("pk"),
|
|
entity_type="project",
|
|
project_id=OuterRef("pk"),
|
|
)
|
|
)
|
|
)
|
|
.annotate(
|
|
member_role=ProjectMember.objects.filter(
|
|
project_id=OuterRef("pk"),
|
|
member_id=self.request.user.id,
|
|
is_active=True,
|
|
).values("role")
|
|
)
|
|
.annotate(
|
|
anchor=DeployBoard.objects.filter(
|
|
entity_name="project",
|
|
entity_identifier=OuterRef("pk"),
|
|
workspace__slug=self.kwargs.get("slug"),
|
|
).values("anchor")
|
|
)
|
|
.annotate(sort_order=Subquery(sort_order))
|
|
.prefetch_related(
|
|
Prefetch(
|
|
"project_projectmember",
|
|
queryset=ProjectMember.objects.filter(
|
|
workspace__slug=self.kwargs.get("slug"), is_active=True
|
|
).select_related("member"),
|
|
to_attr="members_list",
|
|
)
|
|
)
|
|
.distinct()
|
|
)
|
|
|
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
|
def list_detail(self, request, slug):
|
|
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
|
projects = self.get_queryset().order_by("sort_order", "name")
|
|
if WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.GUEST.value,
|
|
).exists():
|
|
projects = projects.filter(
|
|
project_projectmember__member=self.request.user,
|
|
project_projectmember__is_active=True,
|
|
)
|
|
|
|
if WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.MEMBER.value,
|
|
).exists():
|
|
projects = projects.filter(
|
|
Q(
|
|
project_projectmember__member=self.request.user,
|
|
project_projectmember__is_active=True,
|
|
)
|
|
| Q(network=2)
|
|
)
|
|
|
|
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
|
return self.paginate(
|
|
order_by=request.GET.get("order_by", "-created_at"),
|
|
request=request,
|
|
queryset=(projects),
|
|
on_results=lambda projects: ProjectListSerializer(projects, many=True).data,
|
|
)
|
|
|
|
projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
|
|
return Response(projects, status=status.HTTP_200_OK)
|
|
|
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
|
def list(self, request, slug):
|
|
sort_order = ProjectMember.objects.filter(
|
|
member=self.request.user,
|
|
project_id=OuterRef("pk"),
|
|
workspace__slug=self.kwargs.get("slug"),
|
|
is_active=True,
|
|
).values("sort_order")
|
|
|
|
projects = (
|
|
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
|
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
|
.annotate(
|
|
member_role=ProjectMember.objects.filter(
|
|
project_id=OuterRef("pk"),
|
|
member_id=self.request.user.id,
|
|
is_active=True,
|
|
).values("role")
|
|
)
|
|
.annotate(inbox_view=F("intake_view"))
|
|
.annotate(sort_order=Subquery(sort_order))
|
|
.distinct()
|
|
).values(
|
|
"id",
|
|
"name",
|
|
"identifier",
|
|
"sort_order",
|
|
"logo_props",
|
|
"member_role",
|
|
"archived_at",
|
|
"workspace",
|
|
"cycle_view",
|
|
"issue_views_view",
|
|
"module_view",
|
|
"page_view",
|
|
"inbox_view",
|
|
"guest_view_all_features",
|
|
"project_lead",
|
|
"network",
|
|
"created_at",
|
|
"updated_at",
|
|
"created_by",
|
|
"updated_by",
|
|
)
|
|
|
|
if WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.GUEST.value,
|
|
).exists():
|
|
projects = projects.filter(
|
|
project_projectmember__member=self.request.user,
|
|
project_projectmember__is_active=True,
|
|
)
|
|
|
|
if WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.MEMBER.value,
|
|
).exists():
|
|
projects = projects.filter(
|
|
Q(
|
|
project_projectmember__member=self.request.user,
|
|
project_projectmember__is_active=True,
|
|
)
|
|
| Q(network=2)
|
|
)
|
|
return Response(projects, status=status.HTTP_200_OK)
|
|
|
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
|
def retrieve(self, request, slug, pk):
|
|
project = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk).first()
|
|
|
|
if project is None:
|
|
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
member_ids = [str(project_member.member_id) for project_member in project.members_list]
|
|
|
|
if str(request.user.id) not in member_ids:
|
|
if project.network == ProjectNetwork.SECRET.value:
|
|
return Response(
|
|
{"error": "You do not have permission"},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
else:
|
|
return Response(
|
|
{"error": "You are not a member of this project"},
|
|
status=status.HTTP_409_CONFLICT,
|
|
)
|
|
|
|
recent_visited_task.delay(
|
|
slug=slug,
|
|
project_id=pk,
|
|
entity_name="project",
|
|
entity_identifier=pk,
|
|
user_id=request.user.id,
|
|
)
|
|
|
|
serializer = ProjectListSerializer(project)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
|
def create(self, request, slug):
|
|
workspace = Workspace.objects.get(slug=slug)
|
|
|
|
serializer = ProjectSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
|
|
# Add the user as Administrator to the project
|
|
_ = ProjectMember.objects.create(
|
|
project_id=serializer.data["id"],
|
|
member=request.user,
|
|
role=ROLE.ADMIN.value,
|
|
)
|
|
# Also create the issue property for the user
|
|
_ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user)
|
|
|
|
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
|
|
request.user.id
|
|
):
|
|
ProjectMember.objects.create(
|
|
project_id=serializer.data["id"],
|
|
member_id=serializer.data["project_lead"],
|
|
role=ROLE.ADMIN.value,
|
|
)
|
|
# Also create the issue property for the user
|
|
IssueUserProperty.objects.create(
|
|
project_id=serializer.data["id"],
|
|
user_id=serializer.data["project_lead"],
|
|
)
|
|
|
|
State.objects.bulk_create(
|
|
[
|
|
State(
|
|
name=state["name"],
|
|
color=state["color"],
|
|
project=serializer.instance,
|
|
sequence=state["sequence"],
|
|
workspace=serializer.instance.workspace,
|
|
group=state["group"],
|
|
default=state.get("default", False),
|
|
created_by=request.user,
|
|
)
|
|
for state in DEFAULT_STATES
|
|
]
|
|
)
|
|
|
|
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
|
|
|
# Create the model activity
|
|
model_activity.delay(
|
|
model_name="project",
|
|
model_id=str(project.id),
|
|
requested_data=request.data,
|
|
current_instance=None,
|
|
actor_id=request.user.id,
|
|
slug=slug,
|
|
origin=base_host(request=request, is_app=True),
|
|
)
|
|
|
|
serializer = ProjectListSerializer(project)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
def partial_update(self, request, slug, pk=None):
|
|
# try:
|
|
is_workspace_admin = WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.ADMIN.value,
|
|
).exists()
|
|
|
|
is_project_admin = ProjectMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
project_id=pk,
|
|
role=ROLE.ADMIN.value,
|
|
is_active=True,
|
|
).exists()
|
|
|
|
# Return error for if the user is neither workspace admin nor project admin
|
|
if not is_project_admin and not is_workspace_admin:
|
|
return Response(
|
|
{"error": "You don't have the required permissions."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
workspace = Workspace.objects.get(slug=slug)
|
|
|
|
project = Project.objects.get(pk=pk)
|
|
intake_view = request.data.get("inbox_view", project.intake_view)
|
|
current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
|
|
if project.archived_at:
|
|
return Response(
|
|
{"error": "Archived projects cannot be updated"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
serializer = ProjectSerializer(
|
|
project,
|
|
data={**request.data, "intake_view": intake_view},
|
|
context={"workspace_id": workspace.id},
|
|
partial=True,
|
|
)
|
|
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
if intake_view:
|
|
intake = Intake.objects.filter(project=project, is_default=True).first()
|
|
if not intake:
|
|
Intake.objects.create(
|
|
name=f"{project.name} Intake",
|
|
project=project,
|
|
is_default=True,
|
|
)
|
|
|
|
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
|
|
|
model_activity.delay(
|
|
model_name="project",
|
|
model_id=str(project.id),
|
|
requested_data=request.data,
|
|
current_instance=current_instance,
|
|
actor_id=request.user.id,
|
|
slug=slug,
|
|
origin=base_host(request=request, is_app=True),
|
|
)
|
|
serializer = ProjectListSerializer(project)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
def destroy(self, request, slug, pk):
|
|
if (
|
|
WorkspaceMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
is_active=True,
|
|
role=ROLE.ADMIN.value,
|
|
).exists()
|
|
or ProjectMember.objects.filter(
|
|
member=request.user,
|
|
workspace__slug=slug,
|
|
project_id=pk,
|
|
role=ROLE.ADMIN.value,
|
|
is_active=True,
|
|
).exists()
|
|
):
|
|
project = Project.objects.get(pk=pk, workspace__slug=slug)
|
|
project.delete()
|
|
webhook_activity.delay(
|
|
event="project",
|
|
verb="deleted",
|
|
field=None,
|
|
old_value=None,
|
|
new_value=None,
|
|
actor_id=request.user.id,
|
|
slug=slug,
|
|
current_site=base_host(request=request, is_app=True),
|
|
event_id=project.id,
|
|
old_identifier=None,
|
|
new_identifier=None,
|
|
)
|
|
# Delete the project members
|
|
DeployBoard.objects.filter(project_id=pk, workspace__slug=slug).delete()
|
|
|
|
# Delete the user favorite
|
|
UserFavorite.objects.filter(project_id=pk, workspace__slug=slug).delete()
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
else:
|
|
return Response(
|
|
{"error": "You don't have the required permissions."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
|
|
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
|
def post(self, request, slug, project_id):
|
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
project.archived_at = timezone.now()
|
|
project.save()
|
|
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
|
|
return Response({"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK)
|
|
|
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
|
def delete(self, request, slug, project_id):
|
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
project.archived_at = None
|
|
project.save()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class ProjectIdentifierEndpoint(BaseAPIView):
|
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
|
def get(self, request, slug):
|
|
name = request.GET.get("name", "").strip().upper()
|
|
|
|
if name == "":
|
|
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
exists = ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).values("id", "name", "project")
|
|
|
|
return Response({"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK)
|
|
|
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
|
def delete(self, request, slug):
|
|
name = request.data.get("name", "").strip().upper()
|
|
|
|
if name == "":
|
|
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
|
|
return Response(
|
|
{"error": "Cannot delete an identifier of an existing project"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class ProjectUserViewsEndpoint(BaseAPIView):
|
|
def post(self, request, slug, project_id):
|
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
|
|
|
project_member = ProjectMember.objects.filter(member=request.user, project=project, is_active=True).first()
|
|
|
|
if project_member is None:
|
|
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
view_props = project_member.view_props
|
|
default_props = project_member.default_props
|
|
preferences = project_member.preferences
|
|
sort_order = project_member.sort_order
|
|
|
|
project_member.view_props = request.data.get("view_props", view_props)
|
|
project_member.default_props = request.data.get("default_props", default_props)
|
|
project_member.preferences = request.data.get("preferences", preferences)
|
|
project_member.sort_order = request.data.get("sort_order", sort_order)
|
|
|
|
project_member.save()
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class ProjectFavoritesViewSet(BaseViewSet):
|
|
model = UserFavorite
|
|
|
|
def get_queryset(self):
|
|
return self.filter_queryset(
|
|
super()
|
|
.get_queryset()
|
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
.filter(user=self.request.user)
|
|
.select_related("project", "project__project_lead", "project__default_assignee")
|
|
.select_related("workspace", "workspace__owner")
|
|
)
|
|
|
|
def perform_create(self, serializer):
|
|
serializer.save(user=self.request.user)
|
|
|
|
def create(self, request, slug):
|
|
_ = UserFavorite.objects.create(
|
|
user=request.user,
|
|
entity_type="project",
|
|
entity_identifier=request.data.get("project"),
|
|
project_id=request.data.get("project"),
|
|
)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
def destroy(self, request, slug, project_id):
|
|
project_favorite = UserFavorite.objects.get(
|
|
entity_identifier=project_id,
|
|
entity_type="project",
|
|
project=project_id,
|
|
user=request.user,
|
|
workspace__slug=slug,
|
|
)
|
|
project_favorite.delete(soft=False)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class DeployBoardViewSet(BaseViewSet):
|
|
permission_classes = [ProjectMemberPermission]
|
|
serializer_class = DeployBoardSerializer
|
|
model = DeployBoard
|
|
|
|
def list(self, request, slug, project_id):
|
|
project_deploy_board = DeployBoard.objects.filter(
|
|
entity_name="project", entity_identifier=project_id, workspace__slug=slug
|
|
).first()
|
|
|
|
serializer = DeployBoardSerializer(project_deploy_board)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
def create(self, request, slug, project_id):
|
|
comments = request.data.get("is_comments_enabled", False)
|
|
reactions = request.data.get("is_reactions_enabled", False)
|
|
intake = request.data.get("intake", None)
|
|
votes = request.data.get("is_votes_enabled", False)
|
|
views = request.data.get(
|
|
"views",
|
|
{
|
|
"list": True,
|
|
"kanban": True,
|
|
"calendar": True,
|
|
"gantt": True,
|
|
"spreadsheet": True,
|
|
},
|
|
)
|
|
|
|
project_deploy_board, _ = DeployBoard.objects.get_or_create(
|
|
entity_name="project", entity_identifier=project_id, project_id=project_id
|
|
)
|
|
project_deploy_board.intake = intake
|
|
project_deploy_board.view_props = views
|
|
project_deploy_board.is_votes_enabled = votes
|
|
project_deploy_board.is_comments_enabled = comments
|
|
project_deploy_board.is_reactions_enabled = reactions
|
|
|
|
project_deploy_board.save()
|
|
|
|
serializer = DeployBoardSerializer(project_deploy_board)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|