Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move handles from appuser to workspace (with fallback) #585

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
20 changes: 20 additions & 0 deletions app_users/migrations/0024_alter_appuser_handle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.3 on 2025-01-08 12:04

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('app_users', '0023_alter_appusertransaction_workspace'),
('handles', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='appuser',
name='handle',
field=models.OneToOneField(blank=True, default=None, help_text='[deprecated] use workspace.handle instead', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='handles.handle'),
Copy link
Member Author

Choose a reason for hiding this comment

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

adds help_text='[deprecated] ... here

),
]
16 changes: 12 additions & 4 deletions app_users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class AppUser(models.Model):
blank=True,
null=True,
related_name="user",
help_text="[deprecated] use workspace.handle instead",
)

banner_url = CustomURLField(blank=True, default="")
Expand Down Expand Up @@ -225,11 +226,12 @@ def copy_from_firebase_user(self, user: auth.UserRecord) -> "AppUser":
else:
self.balance = 0

if handle := Handle.create_default_for_user(user=self):
self.handle = handle

self.save()
self.get_or_create_personal_workspace()
workspace, _ = self.get_or_create_personal_workspace()

if handle := Handle.create_default_for_user(user=self):
workspace.handle = handle
workspace.save()
Comment on lines +230 to +234
Copy link
Member Author

Choose a reason for hiding this comment

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

for new users, handle should be on the workspace


return self

Expand All @@ -248,6 +250,12 @@ def cached_workspaces(self) -> list["Workspace"]:
).order_by("-is_personal", "-created_at")
) or [self.get_or_create_personal_workspace()[0]]

def get_handle(self) -> Handle | None:
if self.handle:
return self.handle
workspace, _ = self.get_or_create_personal_workspace()
return workspace.handle
Comment on lines +253 to +257
Copy link
Member Author

Choose a reason for hiding this comment

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

cache get_or_create_personal_workspace()?


def get_anonymous_token(self):
return auth.create_custom_token(self.uid).decode()

Expand Down
4 changes: 2 additions & 2 deletions bots/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def help_text(self, workspace: typing.Optional["Workspace"] = None):
return f"{self.get_icon()} Only me + people with a link"
case PublishedRunVisibility.PUBLIC if workspace and workspace.is_personal:
user = workspace.created_by
if user.handle:
profile_url = user.handle.get_app_url()
if handle := (workspace.handle or user.handle):
profile_url = handle.get_app_url()
pretty_profile_url = urls.remove_scheme(profile_url).rstrip("/")
return f'{self.get_icon()} Public on <a href="{pretty_profile_url}" target="_blank">{profile_url}</a>'
else:
Expand Down
12 changes: 7 additions & 5 deletions daras_ai_v2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,8 @@ def _render_author_as_breadcrumb(self, *, is_example: bool, is_root_example: boo
):
if user := self.current_sr_user:
full_name = user.full_name()
link = user.handle and user.handle.get_app_url()
handle = user.get_handle()
link = handle and handle.get_app_url()
else:
full_name = "Deleted User"
link = None
Expand Down Expand Up @@ -1499,8 +1500,9 @@ def render_workspace_author(
name = workspace.created_by.display_name
else:
name = workspace.display_name()
if show_as_link and workspace.is_personal and workspace.created_by.handle:
link = workspace.created_by.handle.get_app_url()
if show_as_link and workspace.is_personal:
handle = workspace.handle or workspace.created_by.handle
Copy link
Member Author

Choose a reason for hiding this comment

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

not using workspace.created_by.get_handle() because personal workspace already

link = handle and handle.get_app_url()
else:
link = None
return cls._render_author(
Expand All @@ -1526,8 +1528,8 @@ def render_author(
return
photo = user.photo_url
name = user.full_name()
if show_as_link and user.handle:
link = user.handle.get_app_url()
if show_as_link and (handle := user.get_handle()):
link = handle.get_app_url()
else:
link = None
return cls._render_author(
Expand Down
115 changes: 77 additions & 38 deletions daras_ai_v2/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
)
from handles.models import Handle

if typing.TYPE_CHECKING:
from workspaces.models import Workspace


class ContributionsSummary(typing.NamedTuple):
total: int
Expand All @@ -41,24 +44,26 @@ class PublicRunsSummary(typing.NamedTuple):


def get_meta_tags_for_profile(user: AppUser):
assert user.handle
handle = user.get_handle()
assert handle
Comment on lines +47 to +48
Copy link
Member Author

Choose a reason for hiding this comment

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

assert (handle := user.get_handle()) is also valid python


return raw_build_meta_tags(
url=user.handle.get_app_url(),
url=handle.get_app_url(),
title=_get_meta_title_for_profile(user),
description=_get_meta_description_for_profile(user) or None,
image=get_profile_image(user),
canonical_url=user.handle.get_app_url(),
canonical_url=handle.get_app_url(),
)


def user_profile_page(request: Request, user: AppUser):
def user_profile_page(request: Request, user: AppUser, handle: Handle | None):
with gui.div(className="mt-3"):
user_profile_header(request, user)
user_profile_header(request, user=user, handle=handle)
gui.html("\n<hr>\n")
user_profile_main_content(user)


def user_profile_header(request, user: AppUser):
def user_profile_header(request: Request, user: AppUser, handle: Handle | None):
if user.banner_url:
with _banner_image_div(user.banner_url, className="my-3"):
pass
Expand Down Expand Up @@ -99,8 +104,9 @@ def user_profile_header(request, user: AppUser):
):
gui.html(f"{icons.edit} Edit Profile")

with gui.tag("p", className="lead text-secondary mb-0"):
gui.html(escape_html(user.handle and user.handle.name or ""))
if handle:
with gui.tag("p", className="lead text-secondary mb-0"):
gui.html(escape_html(handle.name))

if user.bio:
with gui.div(className="mt-2 text-secondary"):
Expand Down Expand Up @@ -254,19 +260,24 @@ def _set_is_uploading_photo(val: bool):
gui.session_state["_uploading_photo"] = val


def edit_user_profile_page(user: AppUser):
_edit_user_profile_header(user)
_edit_user_profile_banner(user)
def edit_user_profile_page(workspace: "Workspace"):
assert workspace.is_personal

_edit_user_profile_header(workspace)
_edit_user_profile_banner(workspace)

colspec = [2, 10] if not _is_uploading_photo() else [6, 6]
photo_col, form_col = gui.columns(colspec)
with photo_col:
_edit_user_profile_photo_section(user)
_edit_user_profile_photo_section(workspace)
with form_col:
_edit_user_profile_form_section(user)
_edit_user_profile_form_section(workspace)


def _edit_user_profile_header(user: AppUser):
def _edit_user_profile_header(workspace: "Workspace"):
user = workspace.created_by
handle = workspace.handle or user.handle # TODO: remove fallback

gui.write("# Update your Profile")

with gui.div(className="mb-3"):
Expand All @@ -276,18 +287,18 @@ def _edit_user_profile_header(user: AppUser):
with gui.tag("span"):
gui.html(
str(furl(settings.APP_BASE_URL) / "/")
+ f"<strong>{escape_html(user.handle.name) if user.handle else 'your-username'}</strong> ",
+ f"<strong>{escape_html(handle.name) if handle else 'your-username'}</strong> ",
)

if user.handle:
if handle:
copy_to_clipboard_button(
f"{icons.copy_solid} Copy",
value=user.handle.get_app_url(),
value=handle.get_app_url(),
type="tertiary",
className="m-0",
)
with gui.link(
to=user.handle.get_app_url(),
to=handle.get_app_url(),
className="btn btn-theme btn-tertiary m-0",
):
gui.html(f"{icons.preview} Preview")
Expand All @@ -314,7 +325,9 @@ def _banner_image_div(url: str | None, **props):
return gui.div(style=style, className=className)


def _edit_user_profile_banner(user: AppUser):
def _edit_user_profile_banner(workspace: "Workspace"):
user = workspace.created_by

def _is_uploading_banner_photo() -> bool:
return bool(gui.session_state.get("_uploading_banner_photo"))

Expand Down Expand Up @@ -383,7 +396,9 @@ def _get_uploading_banner_photo() -> str | None:
gui.rerun()


def _edit_user_profile_photo_section(user: AppUser):
def _edit_user_profile_photo_section(workspace: "Workspace"):
user = workspace.created_by

with gui.div(className="w-100 h-100 d-flex align-items-center flex-column"):
if _is_uploading_photo():
image_div = gui.div(
Expand Down Expand Up @@ -428,23 +443,25 @@ def _edit_user_profile_photo_section(user: AppUser):
gui.rerun()


def _edit_user_profile_form_section(user: AppUser):
def _edit_user_profile_form_section(workspace: "Workspace"):
user = workspace.created_by
current_handle = workspace.handle or user.handle # TODO: remove fallback
user.display_name = gui.text_input("Name", value=user.display_name)

handle_style: dict[str, str] = {}
if handle := gui.text_input(
if new_handle := gui.text_input(
"Username",
value=(user.handle and user.handle.name or ""),
value=current_handle and current_handle.name or "",
style=handle_style,
):
if not user.handle or user.handle.name != handle:
if not current_handle or current_handle.name != new_handle:
try:
Handle(name=handle).full_clean()
Handle(name=new_handle).full_clean()
except ValidationError as e:
gui.error(e.messages[0], icon="")
handle_style["border"] = "1px solid var(--bs-danger)"
else:
gui.success("Handle is available", icon="")
gui.success(f"Handle `@{new_handle}` is available", icon="")
handle_style["border"] = "1px solid var(--bs-success)"

if email := user.email:
Expand Down Expand Up @@ -475,21 +492,36 @@ def _edit_user_profile_form_section(user: AppUser):
):
try:
with transaction.atomic():
if handle and not user.handle:
if new_handle and not current_handle:
# user adds a new handle
user.handle = Handle(name=handle)
user.handle.save()
elif handle and user.handle and user.handle.name != handle:
workspace.handle = Handle(name=new_handle)
workspace.handle.save()
elif (
new_handle and current_handle and current_handle.name != new_handle
):
# user changes existing handle
user.handle.name = handle
user.handle.save()
elif not handle and user.handle:
if workspace.handle:
workspace.handle.name = new_handle
workspace.handle.save()
elif user.handle:
# TODO: remove this once all handles are migrated
user.handle.delete()
user.handle = None
workspace.handle = Handle(name=new_handle)
workspace.handle.save()
elif not new_handle and current_handle:
# user removes existing handle
user.handle.delete()
user.handle = None

if workspace.handle:
workspace.handle.delete()
workspace.handle = None
elif user.handle:
# TODO: remove this once all handles are migrated
user.handle.delete()
user.handle = None
user.full_clean()
workspace.full_clean()
user.save()
workspace.save()
except (ValidationError, IntegrityError) as e:
for m in e.messages:
gui.error(m, icon="⚠️")
Expand All @@ -498,7 +530,13 @@ def _edit_user_profile_form_section(user: AppUser):


def _get_meta_title_for_profile(user: AppUser) -> str:
title = user.display_name if user.display_name else user.handle.name
if user.display_name:
title = user.display_name
elif handle := user.get_handle():
title = handle.name
else:
title = ""

if user.company:
title += f" - {user.company[:15]}"

Expand All @@ -516,7 +554,8 @@ def _get_meta_description_for_profile(user: AppUser) -> str:
if description:
description += f" {META_SEP} "

description += f"{user.handle.name} has "
if handle := user.get_handle():
description += f"{handle.name} has "

public_runs_summary = get_public_runs_summary(user)
contributions_summary = get_contributions_summary(user)
Expand Down
12 changes: 8 additions & 4 deletions handles/admin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from django.contrib import admin

from app_users.admin import AppUserAdmin
from workspaces.admin import WorkspaceAdmin
from .models import Handle


@admin.register(Handle)
class HandleAdmin(admin.ModelAdmin):
search_fields = ["name", "redirect_url"] + [
f"user__{field}" for field in AppUserAdmin.search_fields
]
readonly_fields = ["user", "created_at", "updated_at"]
search_fields = (
["name", "redirect_url"]
+ [f"user__{field}" for field in AppUserAdmin.search_fields]
+ [f"workspace__{field}" for field in WorkspaceAdmin.search_fields]
)
readonly_fields = ["user", "workspace", "created_at", "updated_at"]

list_filter = [
("user", admin.EmptyFieldListFilter),
("workspace", admin.EmptyFieldListFilter),
("redirect_url", admin.EmptyFieldListFilter),
]
Loading
Loading