Merge branch 'feat-views' of github.com:makeplane/plane into feat-views

This commit is contained in:
sriram veeraghanta 2024-01-30 19:10:48 +05:30
commit 0876ea6b55
8 changed files with 371 additions and 53 deletions

View File

@ -7,6 +7,8 @@ from .user import (
UserAdminLiteSerializer, UserAdminLiteSerializer,
UserMeSerializer, UserMeSerializer,
UserMeSettingsSerializer, UserMeSettingsSerializer,
ProfileSerializer,
AccountSerializer,
) )
from .workspace import ( from .workspace import (
WorkSpaceSerializer, WorkSpaceSerializer,
@ -116,7 +118,10 @@ from .inbox import (
from .analytic import AnalyticViewSerializer from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .exporter import ExporterHistorySerializer from .exporter import ExporterHistorySerializer

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
# Module import # Module import
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import User, Workspace, WorkspaceMemberInvite from plane.db.models import User, Workspace, WorkspaceMemberInvite, Profile, Account
from plane.license.models import InstanceAdmin, Instance from plane.license.models import InstanceAdmin, Instance
@ -24,7 +24,6 @@ class UserSerializer(BaseSerializer):
"last_logout_ip", "last_logout_ip",
"last_login_uagent", "last_login_uagent",
"token_updated_at", "token_updated_at",
"is_onboarded",
"is_bot", "is_bot",
"is_password_autoset", "is_password_autoset",
"is_email_verified", "is_email_verified",
@ -52,16 +51,9 @@ class UserMeSerializer(BaseSerializer):
"is_bot", "is_bot",
"is_email_verified", "is_email_verified",
"is_managed", "is_managed",
"is_onboarded",
"is_tour_completed",
"mobile_number", "mobile_number",
"role",
"onboarding_step",
"user_timezone", "user_timezone",
"username", "username",
"theme",
"last_workspace_id",
"use_case",
"is_password_autoset", "is_password_autoset",
"is_email_verified", "is_email_verified",
] ]
@ -84,25 +76,28 @@ class UserMeSettingsSerializer(BaseSerializer):
workspace_invites = WorkspaceMemberInvite.objects.filter( workspace_invites = WorkspaceMemberInvite.objects.filter(
email=obj.email email=obj.email
).count() ).count()
# profile
profile = Profile.objects.get(user=obj)
if ( if (
obj.last_workspace_id is not None profile.last_workspace_id is not None
and Workspace.objects.filter( and Workspace.objects.filter(
pk=obj.last_workspace_id, pk=profile.last_workspace_id,
workspace_member__member=obj.id, workspace_member__member=obj.id,
workspace_member__is_active=True, workspace_member__is_active=True,
).exists() ).exists()
): ):
workspace = Workspace.objects.filter( workspace = Workspace.objects.filter(
pk=obj.last_workspace_id, pk=profile.last_workspace_id,
workspace_member__member=obj.id, workspace_member__member=obj.id,
workspace_member__is_active=True, workspace_member__is_active=True,
).first() ).first()
return { return {
"last_workspace_id": obj.last_workspace_id, "last_workspace_id": profile.last_workspace_id,
"last_workspace_slug": workspace.slug "last_workspace_slug": workspace.slug
if workspace is not None if workspace is not None
else "", else "",
"fallback_workspace_id": obj.last_workspace_id, "fallback_workspace_id": profile.last_workspace_id,
"fallback_workspace_slug": workspace.slug "fallback_workspace_slug": workspace.slug
if workspace is not None if workspace is not None
else "", else "",
@ -197,3 +192,16 @@ class ResetPasswordSerializer(serializers.Serializer):
""" """
new_password = serializers.CharField(required=True, min_length=8) new_password = serializers.CharField(required=True, min_length=8)
class ProfileSerializer(BaseSerializer):
class Meta:
model = Profile
fields = "__all__"
class AccountSerializer(BaseSerializer):
class Meta:
model = Account
fields = "__all__"

View File

@ -3,6 +3,8 @@ from django.urls import path
from plane.app.views import ( from plane.app.views import (
## User ## User
UserEndpoint, UserEndpoint,
ProfileEndpoint,
AccountEndpoint,
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint, UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
@ -39,6 +41,16 @@ urlpatterns = [
), ),
name="users", name="users",
), ),
path(
"users/me/profile/",
ProfileEndpoint.as_view(),
name="accounts",
),
path(
"users/me/accounts/",
AccountEndpoint.as_view(),
name="accounts",
),
path( path(
"users/me/instance-admin/", "users/me/instance-admin/",
UserEndpoint.as_view( UserEndpoint.as_view(

View File

@ -18,6 +18,8 @@ from .user import (
UpdateUserOnBoardedEndpoint, UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint, UpdateUserTourCompletedEndpoint,
UserActivityEndpoint, UserActivityEndpoint,
ProfileEndpoint,
AccountEndpoint,
) )
from .oauth import OauthEndpoint from .oauth import OauthEndpoint
@ -180,7 +182,4 @@ from .webhook import (
WebhookSecretRegenerateEndpoint, WebhookSecretRegenerateEndpoint,
) )
from .dashboard import ( from .dashboard import DashboardEndpoint, WidgetsEndpoint
DashboardEndpoint,
WidgetsEndpoint
)

View File

@ -9,10 +9,19 @@ from plane.app.serializers import (
IssueActivitySerializer, IssueActivitySerializer,
UserMeSerializer, UserMeSerializer,
UserMeSettingsSerializer, UserMeSettingsSerializer,
ProfileSerializer,
AccountSerializer,
) )
from plane.app.views.base import BaseViewSet, BaseAPIView from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember from plane.db.models import (
Account,
User,
IssueActivity,
WorkspaceMember,
ProjectMember,
Profile,
)
from plane.license.models import Instance, InstanceAdmin from plane.license.models import Instance, InstanceAdmin
from plane.utils.paginator import BasePaginator from plane.utils.paginator import BasePaginator
@ -131,24 +140,29 @@ class UserEndpoint(BaseViewSet):
# Deactivate the user # Deactivate the user
user.is_active = False user.is_active = False
user.last_workspace_id = None
user.is_tour_completed = False # Profile updates
user.is_onboarded = False profile = Profile.objects.get(user=user)
user.onboarding_step = {
profile.last_workspace_id = None
profile.is_tour_completed = False
profile.is_onboarded = False
profile.onboarding_step = {
"workspace_join": False, "workspace_join": False,
"profile_complete": False, "profile_complete": False,
"workspace_create": False, "workspace_create": False,
"workspace_invite": False, "workspace_invite": False,
} }
profile.save()
user.save() user.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserOnBoardedEndpoint(BaseAPIView):
def patch(self, request): def patch(self, request):
user = User.objects.get(pk=request.user.id, is_active=True) profile = Profile.objects.get(user_id=request.user.id)
user.is_onboarded = request.data.get("is_onboarded", False) profile.is_onboarded = request.data.get("is_onboarded", False)
user.save() profile.save()
return Response( return Response(
{"message": "Updated successfully"}, status=status.HTTP_200_OK {"message": "Updated successfully"}, status=status.HTTP_200_OK
) )
@ -156,9 +170,11 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
class UpdateUserTourCompletedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView):
def patch(self, request): def patch(self, request):
user = User.objects.get(pk=request.user.id, is_active=True) profile = Profile.objects.get(user_id=request.user.id, is_active=True)
user.is_tour_completed = request.data.get("is_tour_completed", False) profile.is_tour_completed = request.data.get(
user.save() "is_tour_completed", False
)
profile.save()
return Response( return Response(
{"message": "Updated successfully"}, status=status.HTTP_200_OK {"message": "Updated successfully"}, status=status.HTTP_200_OK
) )
@ -177,3 +193,18 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
issue_activities, many=True issue_activities, many=True
).data, ).data,
) )
class ProfileEndpoint(BaseAPIView):
def get(self, request):
profile = Profile.objects.get(user=request.user)
serializer = ProfileSerializer(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
class AccountEndpoint(BaseAPIView):
def get(self, request):
accounts = Account.objects.filter(user=request.user)
serializer = AccountSerializer(accounts, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -0,0 +1,213 @@
# Generated by Django 4.2.7 on 2024-01-26 12:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import plane.db.models.user
def migrate_user_extra_profile(apps, schema_editor):
Profile = apps.get_model("db", "Profile")
User = apps.get_model("db", "User")
Profile.objects.bulk_create(
[
Profile(
user_id=user.get("id"),
theme=user.get("theme"),
is_tour_completed=user.get("is_tour_completed"),
use_case=user.get("use_case"),
is_onboarded=user.get("is_onboarded"),
last_workspace_id=user.get("last_workspace_id"),
billing_address_country=user.get("billing_address_country"),
billing_address=user.get("billing_address"),
has_billing_address=user.get("has_billing_address"),
)
for user in User.objects.values(
"id",
"theme",
"is_tour_completed",
"onboarding_step",
"use_case",
"role",
"is_onboarded",
"last_workspace_id",
"billing_address_country",
"billing_address",
"has_billing_address",
)
],
batch_size=1000,
)
class Migration(migrations.Migration):
dependencies = [
("db", "0058_alter_moduleissue_issue_and_more"),
]
operations = [
migrations.CreateModel(
name="Profile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
("theme", models.JSONField(default=dict)),
("is_tour_completed", models.BooleanField(default=False)),
(
"onboarding_step",
models.JSONField(
default=plane.db.models.user.get_default_onboarding
),
),
("use_case", models.TextField(blank=True, null=True)),
(
"role",
models.CharField(blank=True, max_length=300, null=True),
),
("is_onboarded", models.BooleanField(default=False)),
("last_workspace_id", models.UUIDField(null=True)),
(
"billing_address_country",
models.CharField(default="INDIA", max_length=255),
),
("billing_address", models.JSONField(null=True)),
("has_billing_address", models.BooleanField(default=False)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Profile",
"verbose_name_plural": "Profiles",
"db_table": "profiles",
"ordering": ("-created_at",),
},
),
migrations.RunPython(migrate_user_extra_profile),
migrations.RemoveField(
model_name="user",
name="billing_address",
),
migrations.RemoveField(
model_name="user",
name="billing_address_country",
),
migrations.RemoveField(
model_name="user",
name="has_billing_address",
),
migrations.RemoveField(
model_name="user",
name="is_onboarded",
),
migrations.RemoveField(
model_name="user",
name="is_tour_completed",
),
migrations.RemoveField(
model_name="user",
name="last_workspace_id",
),
migrations.RemoveField(
model_name="user",
name="my_issues_prop",
),
migrations.RemoveField(
model_name="user",
name="onboarding_step",
),
migrations.RemoveField(
model_name="user",
name="role",
),
migrations.RemoveField(
model_name="user",
name="theme",
),
migrations.RemoveField(
model_name="user",
name="use_case",
),
migrations.CreateModel(
name="Account",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
("provider_account_id", models.CharField(max_length=255)),
(
"provider",
models.CharField(
choices=[("google", "Google"), ("github", "Github")]
),
),
("access_token", models.TextField()),
("access_token_expired_at", models.DateTimeField(null=True)),
("refresh_token", models.TextField(blank=True, null=True)),
("refresh_token_expired_at", models.DateTimeField(null=True)),
(
"last_connected_at",
models.DateTimeField(default=django.utils.timezone.now),
),
("metadata", models.JSONField(default=dict)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accounts",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
"db_table": "accounts",
"ordering": ("-created_at",),
},
),
]

View File

@ -1,6 +1,6 @@
from .base import BaseModel from .base import BaseModel
from .user import User from .user import User, Profile, Account
from .workspace import ( from .workspace import (
Workspace, Workspace,

View File

@ -21,6 +21,9 @@ from sentry_sdk import capture_exception
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError from slack_sdk.errors import SlackApiError
# Module imports
from ..mixins import TimeAuditModel
def get_default_onboarding(): def get_default_onboarding():
return { return {
@ -40,12 +43,14 @@ class User(AbstractBaseUser, PermissionsMixin):
primary_key=True, primary_key=True,
) )
username = models.CharField(max_length=128, unique=True) username = models.CharField(max_length=128, unique=True)
# user fields # user fields
mobile_number = models.CharField(max_length=255, blank=True, null=True) mobile_number = models.CharField(max_length=255, blank=True, null=True)
email = models.CharField( email = models.CharField(
max_length=255, null=True, blank=True, unique=True max_length=255, null=True, blank=True, unique=True
) )
# identity
display_name = models.CharField(max_length=255, default="")
first_name = models.CharField(max_length=255, blank=True) first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True) last_name = models.CharField(max_length=255, blank=True)
avatar = models.CharField(max_length=255, blank=True) avatar = models.CharField(max_length=255, blank=True)
@ -72,19 +77,10 @@ class User(AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(default=False) is_staff = models.BooleanField(default=False)
is_email_verified = models.BooleanField(default=False) is_email_verified = models.BooleanField(default=False)
is_password_autoset = models.BooleanField(default=False) is_password_autoset = models.BooleanField(default=False)
is_onboarded = models.BooleanField(default=False)
# random token generated
token = models.CharField(max_length=64, blank=True) token = models.CharField(max_length=64, blank=True)
billing_address_country = models.CharField(max_length=255, default="INDIA")
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
user_timezone = models.CharField(
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
)
last_active = models.DateTimeField(default=timezone.now, null=True) last_active = models.DateTimeField(default=timezone.now, null=True)
last_login_time = models.DateTimeField(null=True) last_login_time = models.DateTimeField(null=True)
last_logout_time = models.DateTimeField(null=True) last_logout_time = models.DateTimeField(null=True)
@ -96,18 +92,17 @@ class User(AbstractBaseUser, PermissionsMixin):
) )
last_login_uagent = models.TextField(blank=True) last_login_uagent = models.TextField(blank=True)
token_updated_at = models.DateTimeField(null=True) token_updated_at = models.DateTimeField(null=True)
last_workspace_id = models.UUIDField(null=True) # my_issues_prop = models.JSONField(null=True)
my_issues_prop = models.JSONField(null=True)
role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False) is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict)
display_name = models.CharField(max_length=255, default="") # timezone
is_tour_completed = models.BooleanField(default=False) USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
onboarding_step = models.JSONField(default=get_default_onboarding) user_timezone = models.CharField(
use_case = models.TextField(blank=True, null=True) max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
)
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"] REQUIRED_FIELDS = ["username"]
objects = UserManager() objects = UserManager()
@ -144,6 +139,60 @@ class User(AbstractBaseUser, PermissionsMixin):
super(User, self).save(*args, **kwargs) super(User, self).save(*args, **kwargs)
class Profile(TimeAuditModel):
# User
user = models.OneToOneField("db.User", on_delete=models.CASCADE, related_name="profile")
# General
theme = models.JSONField(default=dict)
# Onboarding
is_tour_completed = models.BooleanField(default=False)
onboarding_step = models.JSONField(default=get_default_onboarding)
use_case = models.TextField(blank=True, null=True)
role = models.CharField(max_length=300, null=True, blank=True) # job role
is_onboarded = models.BooleanField(default=False)
# Last visited workspace
last_workspace_id = models.UUIDField(null=True)
# address data
billing_address_country = models.CharField(max_length=255, default="INDIA")
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
class Meta:
verbose_name = "Profile"
verbose_name_plural = "Profiles"
db_table = "profiles"
ordering = ("-created_at",)
class Account(TimeAuditModel):
user = models.ForeignKey(
"db.User", on_delete=models.CASCADE, related_name="accounts"
)
provider_account_id = models.CharField(max_length=255)
provider = models.CharField(
choices=(("google", "Google"), ("github", "Github"))
)
access_token = models.TextField()
access_token_expired_at = models.DateTimeField(null=True)
refresh_token = models.TextField(null=True, blank=True)
refresh_token_expired_at = models.DateTimeField(null=True)
last_connected_at = models.DateTimeField(default=timezone.now)
metadata = models.JSONField(default=dict)
class Meta:
verbose_name = "Account"
verbose_name_plural = "Accounts"
db_table = "accounts"
ordering = ("-created_at",)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def send_welcome_slack(sender, instance, created, **kwargs): def send_welcome_slack(sender, instance, created, **kwargs):
try: try:
@ -170,6 +219,7 @@ def create_user_notification(sender, instance, created, **kwargs):
if created and not instance.is_bot: if created and not instance.is_bot:
# Module imports # Module imports
from plane.db.models import UserNotificationPreference from plane.db.models import UserNotificationPreference
UserNotificationPreference.objects.create( UserNotificationPreference.objects.create(
user=instance, user=instance,
) )