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 1 commit
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
111 changes: 75 additions & 36 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,13 +44,15 @@ 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(),
)


Expand All @@ -58,7 +63,7 @@ def user_profile_page(request: Request, user: AppUser):
user_profile_main_content(user)


def user_profile_header(request, user: AppUser):
def user_profile_header(request: Request, user: AppUser):
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 := user.get_handle():
with gui.tag("p", className="lead text-secondary mb-0"):
gui.html(escape_html(handle and handle.name or ""))

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
24 changes: 23 additions & 1 deletion handles/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import re
import warnings

from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, RegexValidator
Expand Down Expand Up @@ -118,14 +119,25 @@ class Meta:
def __str__(self):
return f"@{self.name}"

def clean(self):
def _validate_exclusive(self):
if (
self.has_workspace
and self.has_user
and self.workspace.created_by_id == self.user.id
):
# TODO: remove this once all handles are migrated
return
Comment on lines +123 to +129
Copy link
Member Author

Choose a reason for hiding this comment

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

to handle an in-between state where the same handle can be attached to a personal workspace & its user. won't be needed after cleanup_handles() is run in the script


lookups = [
self.has_redirect,
self.has_workspace,
self.has_user,
]
if sum(lookups) > 1:
raise ValidationError("A handle must be exclusive")

def clean(self):
self._validate_exclusive()
super().clean()

def save(self, *args, **kwargs):
Expand All @@ -134,13 +146,23 @@ def save(self, *args, **kwargs):

@property
def has_user(self):
warnings.warn("deprecated, use `has_workspace` instead", DeprecationWarning)
try:
self.user
except Handle.user.RelatedObjectDoesNotExist:
return False
else:
return True

@property
def has_workspace(self):
try:
self.workspace
except Handle.workspace.RelatedObjectDoesNotExist:
return False
else:
return True

@property
def has_redirect(self):
return bool(self.redirect_url)
Expand Down
Loading
Loading