Compare commits

..

1 Commits

Author SHA1 Message Date
NarayanBavisetti f078a0bb5a chore: rendered the user timezone 2024-11-19 16:30:03 +05:30
19 changed files with 134 additions and 138 deletions
@@ -69,6 +69,7 @@ class IntakeIssueDetailSerializer(BaseSerializer):
"duplicate_issue_detail",
"source",
"issue",
"created_by",
]
read_only_fields = [
"project",
+4 -27
View File
@@ -146,12 +146,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -322,7 +317,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
project = Project.objects.filter(id=asset.project_id).first()
project = Project.objects.filter(id=asset.workspace_id).first()
if project is None:
return
# Delete the previous cover image
@@ -392,13 +387,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -631,13 +620,7 @@ class ProjectAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -755,11 +738,6 @@ class ProjectAssetEndpoint(BaseAPIView):
class ProjectBulkAssetEndpoint(BaseAPIView):
def save_project_cover(self, asset, project_id):
project = Project.objects.get(id=project_id)
project.cover_image_asset_id = asset.id
project.save()
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, entity_id):
asset_ids = request.data.get("asset_ids", [])
@@ -795,7 +773,6 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
assets.update(
project_id=project_id,
)
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(
+48
View File
@@ -48,6 +48,7 @@ from plane.db.models import (
)
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView, BaseViewSet
@@ -200,6 +201,15 @@ class CycleViewSet(BaseViewSet):
)
if data:
datetime_fields = [
"created_at",
"updated_at",
"start_date",
"end_date",
]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
return Response(data, status=status.HTTP_200_OK)
data = queryset.values(
@@ -228,6 +238,15 @@ class CycleViewSet(BaseViewSet):
"version",
"created_by",
)
datetime_fields = [
"created_at",
"updated_at",
"start_date",
"end_date",
]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@@ -276,6 +295,15 @@ class CycleViewSet(BaseViewSet):
)
.first()
)
datetime_fields = [
"created_at",
"updated_at",
"start_date",
"end_date",
]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
model_activity.delay(
@@ -365,6 +393,16 @@ class CycleViewSet(BaseViewSet):
"created_by",
).first()
datetime_fields = [
"created_at",
"updated_at",
"start_date",
"end_date",
]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -440,6 +478,16 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_404_NOT_FOUND,
)
datetime_fields = [
"created_at",
"updated_at",
"start_date",
"end_date",
]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
queryset = queryset.first()
recent_visited_task.delay(
+17
View File
@@ -32,6 +32,7 @@ from plane.db.models import (
Session,
)
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
@@ -48,6 +49,7 @@ class UserEndpoint(BaseViewSet):
def get_object(self):
return self.request.user
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve(self, request):
@@ -57,12 +59,14 @@ class UserEndpoint(BaseViewSet):
status=status.HTTP_200_OK,
)
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve_user_settings(self, request):
serialized_data = UserMeSettingsSerializer(request.user).data
return Response(serialized_data, status=status.HTTP_200_OK)
@cache_response(60 * 60)
def retrieve_instance_admin(self, request):
instance = Instance.objects.first()
is_admin = InstanceAdmin.objects.filter(
@@ -72,9 +76,19 @@ class UserEndpoint(BaseViewSet):
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
)
@invalidate_cache(
path="/api/users/me/",
)
@invalidate_cache(
path="/api/users/me/settings/",
)
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/users/me/")
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
def deactivate(self, request):
# Check all workspace user is active
user = self.get_object()
@@ -221,6 +235,7 @@ class UserSessionEndpoint(BaseAPIView):
class UpdateUserOnBoardedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_onboarded = request.data.get("is_onboarded", False)
@@ -232,6 +247,7 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
class UpdateUserTourCompletedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_tour_completed = request.data.get(
@@ -289,6 +305,7 @@ class ProfileEndpoint(BaseAPIView):
serializer = ProfileSerializer(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache("/api/users/me/settings/")
def patch(self, request):
profile = Profile.objects.get(user=request.user)
serializer = ProfileSerializer(
@@ -44,6 +44,7 @@ from plane.db.models import (
WorkspaceTheme,
)
from plane.app.permissions import ROLE, allow_permission
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
@@ -98,6 +99,9 @@ class WorkSpaceViewSet(BaseViewSet):
.select_related("owner")
)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/instances/", user=False)
def create(self, request):
try:
serializer = WorkSpaceSerializer(data=request.data)
@@ -143,6 +147,7 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_410_GONE,
)
@cache_response(60 * 60 * 2)
@allow_permission(
[
ROLE.ADMIN,
@@ -154,6 +159,8 @@ class WorkSpaceViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@allow_permission(
[
ROLE.ADMIN,
@@ -163,6 +170,13 @@ class WorkSpaceViewSet(BaseViewSet):
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
@invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False
)
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -176,6 +190,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
"owner",
]
@cache_response(60 * 60 * 2)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def get(self, request):
+18 -1
View File
@@ -40,7 +40,7 @@ from plane.db.models import (
WorkspaceMember,
DraftIssue,
)
from plane.utils.cache import invalidate_cache
from plane.utils.cache import cache_response, invalidate_cache
from .. import BaseViewSet
@@ -66,6 +66,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
@cache_response(60 * 60 * 2)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -92,6 +93,12 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
@@ -120,6 +127,16 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
@invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
+1 -1
View File
@@ -42,7 +42,7 @@ app.conf.beat_schedule = {
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0),
"schedule": crontab(hour="*/6"),
},
}
@@ -13,6 +13,7 @@ class InstanceSerializer(BaseSerializer):
model = Instance
exclude = [
"license_key",
"user_count"
]
read_only_fields = [
"id",
+1 -20
View File
@@ -3,7 +3,7 @@ from celery import shared_task
from opentelemetry import trace
# Module imports
from plane.license.models import Instance, InstanceAdmin
from plane.license.models import Instance
from plane.db.models import (
User,
Workspace,
@@ -25,7 +25,6 @@ def instance_traces():
# Check if the instance is registered
instance = Instance.objects.first()
instance_admin = InstanceAdmin.objects.first()
# If instance is None then return
if instance is None:
@@ -53,17 +52,6 @@ def instance_traces():
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute(
"is_support_required", instance.is_support_required
)
span.set_attribute("is_setup_done", instance.is_setup_done)
span.set_attribute(
"is_signup_screen_visited", instance.is_signup_screen_visited
)
span.set_attribute("is_verified", instance.is_verified)
span.set_attribute("edition", instance.edition)
span.set_attribute("domain", instance.domain)
span.set_attribute("is_test", instance.is_test)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
@@ -74,13 +62,6 @@ def instance_traces():
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
if instance_admin:
span.set_attribute("admin_email", instance_admin.user.email)
span.set_attribute(
"admin_name",
f"{instance_admin.user.first_name} {instance_admin.user.last_name}",
)
# Workspace details
for workspace in Workspace.objects.all():
# Count of all models
@@ -1,7 +1,6 @@
# Python imports
import json
import secrets
import os
# Django imports
from django.core.management.base import BaseCommand, CommandError
@@ -9,7 +8,10 @@ from django.utils import timezone
from django.conf import settings
# Module imports
from plane.license.models import Instance, EditiontTypes
from plane.license.models import Instance
from plane.db.models import (
User,
)
from plane.license.bgtasks.tracer import instance_traces
@@ -30,6 +32,7 @@ class Command(BaseCommand):
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
@@ -51,11 +54,11 @@ class Command(BaseCommand):
instance = Instance.objects.create(
instance_name="Plane Community Edition",
instance_id=secrets.token_hex(12),
license_key=None,
current_version=payload.get("version"),
latest_version=payload.get("version"),
last_checked_at=timezone.now(),
is_test=os.environ.get("IS_TEST", "0") == "1",
edition=EditiontTypes.PLANE_CE.value,
user_count=payload.get("user_count", 0),
)
self.stdout.write(self.style.SUCCESS("Instance registered"))
@@ -66,10 +69,9 @@ class Command(BaseCommand):
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
instance.edition = EditiontTypes.PLANE_CE.value
instance.save()
# Call the instance traces task
@@ -1,31 +0,0 @@
# Generated by Django 4.2.15 on 2024-11-19 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('license', '0004_changelog_deleted_at_instance_deleted_at_and_more'),
]
operations = [
migrations.RenameField(
model_name='instance',
old_name='product',
new_name='edition',
),
migrations.RemoveField(
model_name='instance',
name='license_key',
),
migrations.RemoveField(
model_name='instance',
name='user_count',
),
migrations.AddField(
model_name='instance',
name='is_test',
field=models.BooleanField(default=False),
),
]
+1 -6
View File
@@ -1,6 +1 @@
from .instance import (
Instance,
InstanceAdmin,
InstanceConfiguration,
EditiontTypes,
)
from .instance import Instance, InstanceAdmin, InstanceConfiguration
+6 -4
View File
@@ -11,7 +11,7 @@ from plane.db.models import BaseModel
ROLE_CHOICES = ((20, "Admin"),)
class EditiontTypes(Enum):
class ProductTypes(Enum):
PLANE_CE = "plane-ce"
@@ -20,10 +20,11 @@ class Instance(BaseModel):
instance_name = models.CharField(max_length=255)
whitelist_emails = models.TextField(blank=True, null=True)
instance_id = models.CharField(max_length=255, unique=True)
license_key = models.CharField(max_length=256, null=True, blank=True)
current_version = models.CharField(max_length=255)
latest_version = models.CharField(max_length=255, null=True, blank=True)
edition = models.CharField(
max_length=255, default=EditiontTypes.PLANE_CE.value
product = models.CharField(
max_length=255, default=ProductTypes.PLANE_CE.value
)
domain = models.TextField(blank=True)
# Instance specifics
@@ -36,8 +37,9 @@ class Instance(BaseModel):
is_setup_done = models.BooleanField(default=False)
# signup screen
is_signup_screen_visited = models.BooleanField(default=False)
# users
user_count = models.PositiveBigIntegerField(default=0)
is_verified = models.BooleanField(default=False)
is_test = models.BooleanField(default=False)
class Meta:
verbose_name = "Instance"
+1 -5
View File
@@ -18,11 +18,7 @@ export interface IProject {
close_in: number;
created_at: Date;
created_by: string;
// only for uploading the cover image
cover_image_asset?: null;
cover_image?: string;
// only for rendering the cover image
cover_image_url: readonly string;
cover_image_url: string;
cycle_view: boolean;
issue_views_view: boolean;
module_view: boolean;
+4 -13
View File
@@ -3,6 +3,7 @@ import { TUserPermissions } from "./enums";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
export interface IUserLite {
avatar_url: string;
display_name: string;
@@ -13,11 +14,7 @@ export interface IUserLite {
last_name: string;
}
export interface IUser extends IUserLite {
// only for uploading the cover image
cover_image_asset?: string | null;
cover_image?: string | null;
// only for rendering the cover image
cover_image_url: readonly (string | null);
cover_image_url: string | null;
date_joined: string;
email: string;
is_active: boolean;
@@ -93,6 +90,7 @@ export interface IUserTheme {
sidebarBackground: string | undefined;
}
export interface IUserMemberLite extends IUserLite {
email?: string;
}
@@ -155,14 +153,7 @@ export interface IUserProfileProjectSegregation {
id: string;
pending_issues: number;
}[];
user_data: Pick<
IUser,
| "avatar_url"
| "cover_image_url"
| "display_name"
| "first_name"
| "last_name"
> & {
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
date_joined: Date;
user_timezone: string;
};
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
@@ -62,11 +62,6 @@ const WorkspaceDashboardPage = observer(() => {
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
);
const embedRemoveCurrentNotification = useCallback(
() => setCurrentSelectedNotificationId(undefined),
[setCurrentSelectedNotificationId]
);
// clearing up the selected notifications when unmounting the page
useEffect(
() => () => {
@@ -100,12 +95,15 @@ const WorkspaceDashboardPage = observer(() => {
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
) : (
<IssuePeekOverview embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
)}
+1 -5
View File
@@ -70,15 +70,11 @@ const ProfileSettingsPage = observer(() => {
first_name: formData.first_name,
last_name: formData.last_name,
avatar_url: formData.avatar_url,
cover_image_url: formData.cover_image_url,
role: formData.role,
display_name: formData?.display_name,
user_timezone: formData.user_timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false));
setPromiseToast(updateCurrentUserDetail, {
@@ -74,11 +74,6 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
// Upper case identifier
formData.identifier = formData.identifier?.toUpperCase();
const coverImage = formData.cover_image_url;
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (coverImage?.startsWith("http")) {
formData.cover_image = coverImage;
formData.cover_image_asset = null;
}
return createProject(workspaceSlug.toString(), formData)
.then(async (res) => {
+1 -6
View File
@@ -144,15 +144,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
network: formData.network,
identifier: formData.identifier,
description: formData.description,
cover_image_url: formData.cover_image_url,
logo_props: formData.logo_props,
// timezone: formData.timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
if (project.identifier !== formData.identifier)
await projectService