Skip to content
Closed

Staging #6238

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
7800f98
ready for oauth
abu-b-sidq Nov 4, 2024
dc2514c
intermediate
abu-b-sidq Nov 12, 2024
06433d4
issue type and custom fields created
abu-b-sidq Nov 14, 2024
a01a77c
dev complete
abu-b-sidq Dec 10, 2024
c3e4785
testing jenkins
abu-b-sidq Dec 19, 2024
b546d26
testing jenkins
abu-b-sidq Dec 19, 2024
cbb3e68
testing jenkins
abu-b-sidq Dec 19, 2024
100c32e
testing jenkins
abu-b-sidq Dec 19, 2024
ba2628b
testing jenkins
abu-b-sidq Dec 19, 2024
d3db51f
testing jenkins
abu-b-sidq Dec 19, 2024
271a21b
testing jenkins
abu-b-sidq Dec 19, 2024
3fb79be
testing jenkins
abu-b-sidq Dec 19, 2024
a0bee21
testing jenkins
abu-b-sidq Dec 19, 2024
2c0ef2a
testing jenkins
abu-b-sidq Dec 19, 2024
3f9e619
testing jenkins
abu-b-sidq Dec 19, 2024
c3141d7
testing jenkins
abu-b-sidq Dec 19, 2024
33797ca
testing jenkins
abu-b-sidq Dec 19, 2024
d7111d2
testing jenkins
abu-b-sidq Dec 19, 2024
dd1a3cc
testing jenkins
abu-b-sidq Dec 19, 2024
1bc77d5
testing jenkins
abu-b-sidq Dec 19, 2024
b92248b
testing jenkins
abu-b-sidq Dec 19, 2024
07c00f8
testing jenkins
abu-b-sidq Dec 19, 2024
e61227b
testing jenkins
abu-b-sidq Dec 19, 2024
99f593a
testing jenkins
abu-b-sidq Dec 19, 2024
d6804eb
testing jenkins
abu-b-sidq Dec 19, 2024
26e25ac
testing jenkins
abu-b-sidq Dec 19, 2024
74123dd
testing jenkins
abu-b-sidq Dec 19, 2024
0fc8bb0
testing jenkins
abu-b-sidq Dec 19, 2024
093245b
testing jenkins
abu-b-sidq Dec 19, 2024
d4948e5
testing jenkins
abu-b-sidq Dec 19, 2024
a86cbc3
testing jenkins
abu-b-sidq Dec 19, 2024
55019e5
testing jenkins
abu-b-sidq Dec 19, 2024
5fd3638
testing jenkins
abu-b-sidq Dec 19, 2024
7717611
testing jenkins
abu-b-sidq Dec 19, 2024
f54c03c
testing jenkins
abu-b-sidq Dec 19, 2024
e99955f
testing jenkins
abu-b-sidq Dec 19, 2024
f25596f
testing jenkins
abu-b-sidq Dec 19, 2024
5be1055
testing jenkins
abu-b-sidq Dec 19, 2024
17d2ee0
testing jenkins
abu-b-sidq Dec 19, 2024
102dc72
testing jenkins
abu-b-sidq Dec 19, 2024
abcaa3f
testing jenkins
abu-b-sidq Dec 19, 2024
0d7efc8
testing jenkins
abu-b-sidq Dec 19, 2024
ab4a1e5
testing jenkins
abu-b-sidq Dec 19, 2024
8b6fac6
testing jenkins
abu-b-sidq Dec 19, 2024
aa5ec50
testing jenkins
abu-b-sidq Dec 19, 2024
809a63b
testing jenkins
abu-b-sidq Dec 19, 2024
f4b5273
testing jenkins
abu-b-sidq Dec 19, 2024
4f272e9
testing jenkins
abu-b-sidq Dec 19, 2024
f6961ef
testing jenkins
abu-b-sidq Dec 19, 2024
e8fde6e
testing jenkins
abu-b-sidq Dec 19, 2024
ecd4c78
testing jenkins
abu-b-sidq Dec 19, 2024
71880d5
testing jenkins
abu-b-sidq Dec 19, 2024
40a209b
testing jenkins
abu-b-sidq Dec 19, 2024
c4e76bc
testing jenkins
abu-b-sidq Dec 19, 2024
a180116
testing jenkins
abu-b-sidq Dec 19, 2024
8e926b1
testing jenkins
abu-b-sidq Dec 19, 2024
e8c8eaf
testing jenkins
abu-b-sidq Dec 19, 2024
b2e1a72
testing jenkins
abu-b-sidq Dec 19, 2024
c9df844
testing jenkins
abu-b-sidq Dec 19, 2024
555ee5c
testing jenkins
abu-b-sidq Dec 19, 2024
19166f6
testing jenkins
abu-b-sidq Dec 19, 2024
574033c
testing jenkins
abu-b-sidq Dec 19, 2024
0b836dd
testing jenkins
abu-b-sidq Dec 19, 2024
805710d
testing jenkins
abu-b-sidq Dec 19, 2024
a08074b
testing jenkins
abu-b-sidq Dec 19, 2024
caa9341
testing jenkins
abu-b-sidq Dec 19, 2024
240b47a
testing jenkins
abu-b-sidq Dec 19, 2024
3e66925
testing jenkins
abu-b-sidq Dec 19, 2024
c861bc9
testing jenkins
abu-b-sidq Dec 19, 2024
c0fb7b6
testing jenkins
abu-b-sidq Dec 19, 2024
8964187
testing jenkins
abu-b-sidq Dec 19, 2024
50e257e
testing jenkins
abu-b-sidq Dec 19, 2024
1747cef
testing jenkins
abu-b-sidq Dec 19, 2024
150410d
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
2ae579f
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
3c34f59
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
6fc92fd
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
ec07052
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
c2a36cd
testing jenkins
abu-b-sidq Dec 19, 2024
ac39e7e
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
7c5f5ea
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
9bec624
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
2c5c9b7
Testing Multiple Dev
abu-b-sidq Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion admin/Dockerfile.admin
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,6 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1

EXPOSE 3000
EXPOSE 3000

CMD node admin/server.js admin
9 changes: 5 additions & 4 deletions admin/ce/components/common/upgrade-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { getButtonStyling } from "@plane/ui";
import { cn } from "@/helpers/common.helper";

export const UpgradeButton: React.FC = () => (
<a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Available on One
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
<a></a>
// <a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
// Available on One
// <SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
// </a>
);
11 changes: 0 additions & 11 deletions admin/core/components/admin-sidebar/help-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,6 @@ export const HelpSection: FC = observer(() => {
{!isSidebarCollapsed && "Redirect to plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
</button>
</Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
Expand Down
25 changes: 24 additions & 1 deletion apiserver/Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,30 @@ RUN mkdir -p /code/plane/logs
RUN chmod +x ./bin/*
RUN chmod -R 777 /code

# Expose container port and run entry point script
EXPOSE 8000

# ENV N8N_CONFIG_FILES=/home/node/config/env.json

# Declare a build argument
ARG ENV_FILE_PATH

# Use the build argument
COPY ${ENV_FILE_PATH} /code/plane/file.env

# Export the environment file path as an image environment variable
ENV ENV_FILE_PATH=${ENV_FILE_PATH}

CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \
gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -; \
elif [ "${ENV_TYPE}" = "celery" ]; then \
celery -A plane worker -l info; \
elif [ "${ENV_TYPE}" = "celery-beat" ]; then \
celery -A plane beat -l info; \
else \
echo "Unknown ENV_TYPE: ${ENV_TYPE}"; \
exit 1; \
fi

# Expose container port and run entry point script


13 changes: 12 additions & 1 deletion apiserver/plane/api/middleware/api_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

# Module imports
from plane.db.models import APIToken

from django.conf import settings
from django.contrib.auth import get_user_model

class APIKeyAuthentication(authentication.BaseAuthentication):
"""
Expand All @@ -23,6 +24,12 @@ def get_api_token(self, request):
return request.headers.get(self.auth_header_name)

def validate_api_token(self, token):
# Check if the token matches the static token from settings
User = get_user_model()
if token == settings.STATIC_API_TOKEN:
user = User.objects.filter(is_superuser=True).first()
self.rewite_project_id_in_url()
return (user, token)
Comment on lines +27 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Static API token implementation needs review

The implementation of static token authentication raises several security concerns:

  1. Using a static token increases the risk of token exposure
  2. Automatically selecting the first superuser is potentially dangerous
  3. No rate limiting or additional validation is implemented

Consider implementing:

  • Token rotation mechanism
  • Specific service account instead of arbitrary superuser
  • Rate limiting for static token authentication

try:
api_token = APIToken.objects.get(
Q(
Expand All @@ -40,6 +47,10 @@ def validate_api_token(self, token):
api_token.save(update_fields=["last_used"])
return (api_token.user, api_token.token)

def rewite_project_id_in_url(self):
pass
# import pdb;pdb.set_trace()

Comment on lines +50 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove debug code and implement or remove empty method

Issues found:

  1. Method name has a typo (rewite should be rewrite)
  2. Empty implementation with commented debugger statement
  3. Method is called but has no effect

Either:

  1. Implement the method properly with the intended URL rewriting logic, or
  2. Remove the method and its call if it's not needed
-    def rewite_project_id_in_url(self):
-        pass
-        # import pdb;pdb.set_trace()
+    def rewrite_project_id_in_url(self):
+        # TODO: Implement URL rewriting logic
+        raise NotImplementedError("URL rewriting not implemented")

Committable suggestion skipped: line range outside the PR's diff.

def authenticate(self, request):
token = self.get_api_token(request=request)
if not token:
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
IssueExpandSerializer,
IssueLiteSerializer,
)
from .issue_type import IssueTypeSerializer, IssueTypeCustomPropertySerializer
from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import (
Expand Down
Empty file.
119 changes: 108 additions & 11 deletions apiserver/plane/api/serializers/issue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Django imports
from django.utils import timezone
from lxml import html
import uuid

# Third party imports
from rest_framework import serializers
Expand All @@ -19,8 +20,8 @@
ProjectMember,
State,
User,
IssueCustomProperty
)

from .base import BaseSerializer
from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer
Expand All @@ -32,6 +33,26 @@
from django.core.validators import URLValidator


def is_uuid(value):
try:
uuid_obj = uuid.UUID(str(value)) # Convert to string in case it's not already
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused variable 'uuid_obj'
Static analysis flags this variable as unused, and it can be removed without affecting functionality.

-        uuid_obj = uuid.UUID(str(value))  # Convert to string in case it's not already
+        uuid.UUID(str(value))  # Convert to string in case it's not already
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uuid_obj = uuid.UUID(str(value)) # Convert to string in case it's not already
uuid.UUID(str(value)) # Convert to string in case it's not already
🧰 Tools
🪛 Ruff (0.8.2)

38-38: Local variable uuid_obj is assigned to but never used

Remove assignment to unused variable uuid_obj

(F841)

return True
except (ValueError, TypeError):
return False

class IssueCustomPropertySerializer(BaseSerializer):
class Meta:
model = IssueCustomProperty
fields = ["key", "value", "issue_type_custom_property"]
read_only_fields = [
"id",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]

class IssueSerializer(BaseSerializer):
assignees = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(
Expand All @@ -54,6 +75,9 @@ class IssueSerializer(BaseSerializer):
required=False,
allow_null=True,
)
custom_properties = IssueCustomPropertySerializer(many=True, required=False)

created_by = serializers.CharField(required=False)

class Meta:
model = Issue
Expand Down Expand Up @@ -127,12 +151,30 @@ def validate(self, data):
"Parent is not valid issue_id please pass a valid issue_id"
)

if not is_uuid(data['created_by']):
if User.objects.filter(username=data['created_by']).exists():
data['created_by']= User.objects.get(username=data['created_by'])
else:
user_data = {
"email": data['created_by'] + '@plane-shipsy.com',
"username": data['created_by'],
}
from plane.api.views import ProjectMemberAPIEndpoint
PMObj = ProjectMemberAPIEndpoint()
user = PMObj.create_user(user_data)
PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data)
PMObj.create_project_member(self.context.get("project_id"), user_data)
data['created_by'] = user

Comment on lines +154 to +168
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Auto user creation raises security and data integrity concerns

The verification reveals several concerns with the current implementation:

  • Users are created with auto-generated passwords and marked as inactive (is_active=False)
  • The @plane-shipsy.com domain is used across multiple components including authentication flows
  • Magic code authentication specifically handles this domain, suggesting it's part of a broader system
  • Created users are automatically added to workspaces and projects without proper validation

Key issues:

  • The auto-creation bypasses normal user registration flows and security checks
  • Using a fixed domain pattern (@plane-shipsy.com) could lead to namespace conflicts
  • Inactive users with auto-set passwords pose potential security risks
  • No validation of workspace/project access permissions during auto-assignment
🔗 Analysis chain

Automatic user creation logic
Automatically creating users, assigning them to a workspace, and generating semi-fake email addresses is convenient, but ensure it aligns with your user data policies, particularly for email domain usage.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching deeper references to auto user creation to confirm there's no conflicting or partial usage
ast-grep --pattern $'class $_ {
  $$$
  create_user($_) {
    $$$
  }
  $$$
}' 

Length of output: 79


Script:

#!/bin/bash
# Let's search for the create_user method implementation and its usages
rg -A 10 "def create_user" 

# Also search for any other occurrences of auto user creation patterns
rg -A 5 "@plane-shipsy.com"

# Search for related view implementation
rg "class ProjectMemberAPIEndpoint"

Length of output: 4843

print(data)
return data



def create(self, validated_data):
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)

custom_properties = validated_data.pop("custom_properties", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
default_assignee_id = self.context["default_assignee_id"]
Expand Down Expand Up @@ -198,13 +240,31 @@ def create(self, validated_data):
],
batch_size=10,
)
if custom_properties is not None and len(custom_properties):
IssueCustomProperty.objects.bulk_create(
[
IssueCustomProperty(
key=custom_property['key'],
value=custom_property['value'],
issue_type_custom_property=custom_property['issue_type_custom_property'],
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for custom_property in custom_properties
],
batch_size=10,
)

return issue

def update(self, instance, validated_data):
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)

custom_properties = validated_data.pop("custom_properties", None)

# Related models
project_id = instance.project_id
workspace_id = instance.workspace_id
Expand Down Expand Up @@ -244,7 +304,24 @@ def update(self, instance, validated_data):
],
batch_size=10,
)

if custom_properties is not None:
IssueCustomProperty.objects.filter(issue=instance).delete()
IssueCustomProperty.objects.bulk_create(
[
IssueCustomProperty(
key=custom_property['key'],
value=custom_property['value'],
issue_type_custom_property=custom_property['issue_type_custom_property'],
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for custom_property in custom_properties
],
batch_size=10,
)
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
return super().update(instance, validated_data)
Expand Down Expand Up @@ -363,26 +440,27 @@ class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = [
"id",
"created_by",
"updated_by",
"created_at",
"updated_at",
"workspace",
"project",
"issue",
"updated_by",
"updated_at",
]


class IssueCommentSerializer(BaseSerializer):
is_member = serializers.BooleanField(read_only=True)

actor_detail = UserLiteSerializer(read_only=True, source="actor")
created_by = serializers.CharField(required=False)
class Meta:
model = IssueComment
read_only_fields = [
"id",
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
Expand All @@ -398,13 +476,32 @@ def validate(self, data):
parsed = html.fromstring(data["comment_html"])
parsed_str = html.tostring(parsed, encoding="unicode")
data["comment_html"] = parsed_str

except Exception:
# if not is_uuid(data['created_by']):
# if User.objects.filter(username=data['created_by']).exists():
# data['created_by']= User.objects.get(username=data['created_by'])
# else:
# user_data = {
# "email": data['created_by'] + '@plane-shipsy.com',
# "username": data['created_by'],
# }
# from plane.api.views import ProjectMemberAPIEndpoint
# PMObj = ProjectMemberAPIEndpoint()
# user = PMObj.create_user(user_data)
# PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data)
# PMObj.create_project_member(self.context.get("project_id"), user_data)
# data['created_by'] = user

print(data)
except Exception as e:
print(e)
raise serializers.ValidationError("Invalid HTML passed")
return data


class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
# issue_detail = IssueFlatSerializer(read_only=True, source="issue")
# project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = IssueActivity
exclude = [
Expand Down
44 changes: 44 additions & 0 deletions apiserver/plane/api/serializers/issue_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from rest_framework import serializers

from plane.db.models import IssueType, IssueTypeCustomProperty

from .base import BaseSerializer

class IssueTypeSerializer(BaseSerializer):
def validate(self, data):
data['workspace_id'] = self.context["workspace_id"]
return data
class Meta:
model = IssueType
read_only_fields = [
"id",
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
exclude = [
"created_by",
"updated_by",
]

class IssueTypeCustomPropertySerializer(BaseSerializer):
class Meta:
model = IssueTypeCustomProperty
fields = "__all__"
read_only_fields = [
"id",
"issue_type",
"created_at",
"updated_at",
"created_by",
"updated_by",
"deleted_at"
]

def create(self, validated_data):
return IssueTypeCustomProperty.objects.create(
**validated_data,
issue_type_id=self.context["issue_type_id"]
)
Comment on lines +40 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for missing context

The create method assumes issue_type_id exists in the context without validation.

Add proper error handling:

     def create(self, validated_data):
+        issue_type_id = self.context.get('issue_type_id')
+        if not issue_type_id:
+            raise serializers.ValidationError("issue_type_id is required in context")
         return IssueTypeCustomProperty.objects.create(
             **validated_data,
-            issue_type_id=self.context["issue_type_id"]
+            issue_type_id=issue_type_id
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create(self, validated_data):
return IssueTypeCustomProperty.objects.create(
**validated_data,
issue_type_id=self.context["issue_type_id"]
)
def create(self, validated_data):
issue_type_id = self.context.get('issue_type_id')
if not issue_type_id:
raise serializers.ValidationError("issue_type_id is required in context")
return IssueTypeCustomProperty.objects.create(
**validated_data,
issue_type_id=issue_type_id
)

3 changes: 2 additions & 1 deletion apiserver/plane/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)

from .base import BaseSerializer
from .issue_type import IssueTypeSerializer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused import

The IssueTypeSerializer import is not used anywhere in this file.

-from .issue_type import IssueTypeSerializer
🧰 Tools
🪛 Ruff (0.8.2)

12-12: .issue_type.IssueTypeSerializer imported but unused

Remove unused import: .issue_type.IssueTypeSerializer

(F401)



class ProjectSerializer(BaseSerializer):
Expand All @@ -30,7 +31,6 @@ class Meta:
"workspace",
"created_at",
"updated_at",
"created_by",
"updated_by",
"deleted_at",
"cover_image_url",
Expand Down Expand Up @@ -104,3 +104,4 @@ class Meta:
"cover_image_url",
]
read_only_fields = fields

Loading