Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0eafbb6
[WEB-3494] fix: size of created at value #7112
JayashTripathy May 26, 2025
5a208cb
[WEB-2403] fix: alignment of project states in collapsed view #7114
JayashTripathy May 26, 2025
4e485d6
[WEB-4160] fix: close the context menu after select #7113
JayashTripathy May 26, 2025
78cc327
[WEB-3707] pytest based test suite for apiserver (#7010)
dheeru0198 May 26, 2025
04c7c53
chore(deps): bump requests (#7120)
dependabot[bot] May 26, 2025
b4bc499
[WEB-4130] fix: cycle charts minor optimizations (#7123)
gakshita May 27, 2025
a3a5809
[WEB-4166] chore: projects app sidebar accessibility (#7115)
aaryan610 May 27, 2025
e388a9a
[WIKI-181] refactor: file plugins and types (#7074)
aaryan610 May 27, 2025
26b62c4
fix: tsup version 8.4.0
sriramveeraghanta May 27, 2025
141cb17
fix: Optimize image uploads in Editor (#7129)
aaryan610 May 28, 2025
4a97d7c
fix: adding url validations for workspace name and user name
sriramveeraghanta May 29, 2025
b16a585
[WIKI-343] [WIKI-312] Fix: html characters (#7049)
iam-vipin May 30, 2025
01b685e
[WIKI-181] refactor: invalid file handling #7139
aaryan610 May 30, 2025
cb92108
[WEB-4197] chore: auth forms semantics and accessibility #7128
aaryan610 May 30, 2025
a0a6974
[WEB-3787] fix: project joining date (#7127)
sangeethailango May 30, 2025
099a1cc
[WEB-3863] fix: links error handling #7126
gakshita May 30, 2025
046a8a1
[WEB-4189] chore: add tailwind container-queries plugin #7125
aaryan610 May 30, 2025
445c819
[WEB-4172] feat: Crawl work item links for title and favicon (#7117)
sriramveeraghanta May 30, 2025
41c2aef
[WEB-3998] feat: settings page revamp (#6959)
sangeethailango May 30, 2025
322af8c
[WEB-4223] fix: remove build process from utils package #7138
sriramveeraghanta May 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ mediafiles
.env
.DS_Store
logs/
htmlcov/
.coverage

node_modules/
assets/dist/
Expand Down
25 changes: 25 additions & 0 deletions apiserver/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[run]
source = plane
omit =
*/tests/*
*/migrations/*
*/settings/*
*/wsgi.py
*/asgi.py
*/urls.py
manage.py
*/admin.py
*/apps.py

[report]
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
raise NotImplementedError
if __name__ == .__main__.
pass
raise ImportError

[html]
directory = htmlcov
3 changes: 2 additions & 1 deletion apiserver/plane/app/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ class Meta:
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
fields = ("id", "role", "member", "project", "created_at")
read_only_fields = ["created_at"]


class ProjectMemberInviteSerializer(BaseSerializer):
Expand Down
16 changes: 16 additions & 0 deletions apiserver/plane/app/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@

# Module import
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from plane.utils.url import contains_url

from .base import BaseSerializer


class UserSerializer(BaseSerializer):
def validate_first_name(self, value):
if contains_url(value):
raise serializers.ValidationError("First name cannot contain a URL.")
return value

def validate_last_name(self, value):
if contains_url(value):
raise serializers.ValidationError("Last name cannot contain a URL.")
return value

class Meta:
model = User
# Exclude password field from the serializer
Expand Down Expand Up @@ -99,11 +110,16 @@ def get_workspace(self, obj):
workspace_member__member=obj.id,
workspace_member__is_active=True,
).first()
logo_asset_url = workspace.logo_asset.asset_url if workspace.logo_asset is not None else ""
return {
"last_workspace_id": profile.last_workspace_id,
"last_workspace_slug": (
workspace.slug if workspace is not None else ""
),
"last_workspace_name": (
workspace.name if workspace is not None else ""
),
"last_workspace_logo": (logo_asset_url),
"fallback_workspace_id": profile.last_workspace_id,
"fallback_workspace_slug": (
workspace.slug if workspace is not None else ""
Expand Down
13 changes: 13 additions & 0 deletions apiserver/plane/app/serializers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,34 @@
WorkspaceUserPreference,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.url import contains_url

# Django imports
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
import re


class WorkSpaceSerializer(DynamicBaseSerializer):
total_members = serializers.IntegerField(read_only=True)
logo_url = serializers.CharField(read_only=True)
role = serializers.IntegerField(read_only=True)

def validate_name(self, value):
# Check if the name contains a URL
if contains_url(value):
raise serializers.ValidationError("Name must not contain URLs")
return value

def validate_slug(self, value):
# Check if the slug is restricted
if value in RESTRICTED_WORKSPACE_SLUGS:
raise serializers.ValidationError("Slug is not valid")
# Slug should only contain alphanumeric characters, hyphens, and underscores
if not re.match(r"^[a-zA-Z0-9_-]+$", value):
raise serializers.ValidationError(
"Slug can only contain letters, numbers, hyphens (-), and underscores (_)"
)
return value

class Meta:
Expand Down
16 changes: 16 additions & 0 deletions apiserver/plane/app/views/issue/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import IssueLink
from plane.bgtasks.issue_activities_task import issue_activity
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
from plane.utils.host import base_host


Expand Down Expand Up @@ -44,6 +45,9 @@ def create(self, request, slug, project_id, issue_id):
serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
crawl_work_item_link_title(
serializer.data.get("id"), serializer.data.get("url")
)
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
Expand All @@ -55,6 +59,10 @@ def create(self, request, slug, project_id, issue_id):
notification=True,
origin=base_host(request=request, is_app=True),
)

issue_link = self.get_queryset().get(id=serializer.data.get("id"))
serializer = IssueLinkSerializer(issue_link)

return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Expand All @@ -66,9 +74,14 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)

serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
crawl_work_item_link_title(
serializer.data.get("id"), serializer.data.get("url")
)

issue_activity.delay(
type="link.activity.updated",
requested_data=requested_data,
Expand All @@ -80,6 +93,9 @@ def partial_update(self, request, slug, project_id, issue_id, pk):
notification=True,
origin=base_host(request=request, is_app=True),
)
issue_link = self.get_queryset().get(id=serializer.data.get("id"))
serializer = IssueLinkSerializer(issue_link)

return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Expand Down
20 changes: 20 additions & 0 deletions apiserver/plane/app/views/workspace/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io
import os
from datetime import date
import uuid

from dateutil.relativedelta import relativedelta
from django.db import IntegrityError
Expand Down Expand Up @@ -35,6 +36,7 @@
Workspace,
WorkspaceMember,
WorkspaceTheme,
Profile
)
from plane.app.permissions import ROLE, allow_permission
from django.utils.decorators import method_decorator
Expand All @@ -43,6 +45,7 @@
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.workspace_seed_task import workspace_seed
from plane.utils.url import contains_url


class WorkSpaceViewSet(BaseViewSet):
Expand Down Expand Up @@ -109,6 +112,12 @@ def create(self, request):
status=status.HTTP_400_BAD_REQUEST,
)

if contains_url(name):
return Response(
{"error": "Name cannot contain a URL"},
status=status.HTTP_400_BAD_REQUEST,
)

if serializer.is_valid(raise_exception=True):
serializer.save(owner=request.user)
# Create Workspace member
Expand Down Expand Up @@ -150,8 +159,19 @@ def list(self, request, *args, **kwargs):
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)


def remove_last_workspace_ids_from_user_settings(self, id: uuid.UUID) -> None:
"""
Remove the last workspace id from the user settings
"""
Profile.objects.filter(last_workspace_id=id).update(last_workspace_id=None)
return

@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
# Get the workspace
workspace = self.get_object()
self.remove_last_workspace_ids_from_user_settings(workspace.id)
return super().destroy(request, *args, **kwargs)


Expand Down
30 changes: 15 additions & 15 deletions apiserver/plane/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
# credentials
path("sign-in/", SignInAuthEndpoint.as_view(), name="sign-in"),
path("sign-up/", SignUpAuthEndpoint.as_view(), name="sign-up"),
path("spaces/sign-in/", SignInAuthSpaceEndpoint.as_view(), name="sign-in"),
path("spaces/sign-up/", SignUpAuthSpaceEndpoint.as_view(), name="sign-in"),
path("spaces/sign-in/", SignInAuthSpaceEndpoint.as_view(), name="space-sign-in"),
path("spaces/sign-up/", SignUpAuthSpaceEndpoint.as_view(), name="space-sign-up"),
# signout
path("sign-out/", SignOutAuthEndpoint.as_view(), name="sign-out"),
path("spaces/sign-out/", SignOutAuthSpaceEndpoint.as_view(), name="sign-out"),
path("spaces/sign-out/", SignOutAuthSpaceEndpoint.as_view(), name="space-sign-out"),
# csrf token
path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"),
# Magic sign in
Expand All @@ -56,56 +56,56 @@
path(
"spaces/magic-generate/",
MagicGenerateSpaceEndpoint.as_view(),
name="magic-generate",
name="space-magic-generate",
),
path(
"spaces/magic-sign-in/",
MagicSignInSpaceEndpoint.as_view(),
name="magic-sign-in",
name="space-magic-sign-in",
),
path(
"spaces/magic-sign-up/",
MagicSignUpSpaceEndpoint.as_view(),
name="magic-sign-up",
name="space-magic-sign-up",
),
## Google Oauth
path("google/", GoogleOauthInitiateEndpoint.as_view(), name="google-initiate"),
path("google/callback/", GoogleCallbackEndpoint.as_view(), name="google-callback"),
path(
"spaces/google/",
GoogleOauthInitiateSpaceEndpoint.as_view(),
name="google-initiate",
name="space-google-initiate",
),
path(
"google/callback/",
"spaces/google/callback/",
GoogleCallbackSpaceEndpoint.as_view(),
name="google-callback",
name="space-google-callback",
),
## Github Oauth
path("github/", GitHubOauthInitiateEndpoint.as_view(), name="github-initiate"),
path("github/callback/", GitHubCallbackEndpoint.as_view(), name="github-callback"),
path(
"spaces/github/",
GitHubOauthInitiateSpaceEndpoint.as_view(),
name="github-initiate",
name="space-github-initiate",
),
path(
"spaces/github/callback/",
GitHubCallbackSpaceEndpoint.as_view(),
name="github-callback",
name="space-github-callback",
),
## Gitlab Oauth
path("gitlab/", GitLabOauthInitiateEndpoint.as_view(), name="gitlab-initiate"),
path("gitlab/callback/", GitLabCallbackEndpoint.as_view(), name="gitlab-callback"),
path(
"spaces/gitlab/",
GitLabOauthInitiateSpaceEndpoint.as_view(),
name="gitlab-initiate",
name="space-gitlab-initiate",
),
path(
"spaces/gitlab/callback/",
GitLabCallbackSpaceEndpoint.as_view(),
name="gitlab-callback",
name="space-gitlab-callback",
),
# Email Check
path("email-check/", EmailCheckEndpoint.as_view(), name="email-check"),
Expand All @@ -120,12 +120,12 @@
path(
"spaces/forgot-password/",
ForgotPasswordSpaceEndpoint.as_view(),
name="forgot-password",
name="space-forgot-password",
),
path(
"spaces/reset-password/<uidb64>/<token>/",
ResetPasswordSpaceEndpoint.as_view(),
name="forgot-password",
name="space-forgot-password",
),
path("change-password/", ChangePasswordEndpoint.as_view(), name="forgot-password"),
path("set-password/", SetUserPasswordEndpoint.as_view(), name="set-password"),
Expand Down
Loading