Skip to content

Commit

Permalink
remove deprecated appuser handles in favor of workspace handles (#591)
Browse files Browse the repository at this point in the history
* move handles from appuser to workspace (with fallback)

* fix: routing logic for handle page

* add handle<>workspace connection to admin pages

* fix: use correct handle in views.py

* pass down handle for /{handle} route

* use workspace.handle or request.user.handle vs user.get_handle()

* replace Handle.create_default_for_user with Handle.create_default_for_workspace

* update set_default_handles script to set handle on team workspaces

* remove handles from appuser model

* remove AppUser.handle

* only create workspace-handle when not anonymous

* rename conflicting migration

* update create_fixture script to export workspace.handle (and not user.handle)

* add new fixture file without app_users.handle
  • Loading branch information
nikochiko authored Feb 21, 2025
1 parent ae53ef3 commit 76b80be
Show file tree
Hide file tree
Showing 15 changed files with 93 additions and 114 deletions.
5 changes: 1 addition & 4 deletions app_users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class AppUserAdmin(admin.ModelAdmin):
"Profile Options",
{
"fields": [
"handle",
"display_name",
"bio",
("company", "github_username"),
Expand All @@ -72,7 +71,6 @@ class AppUserAdmin(admin.ModelAdmin):
]
list_display = [
"uid",
"handle",
"display_name",
"email",
"phone_number",
Expand All @@ -84,7 +82,6 @@ class AppUserAdmin(admin.ModelAdmin):
"display_name",
"email",
"phone_number",
"handle__name",
]
list_filter = [
"is_anonymous",
Expand All @@ -108,7 +105,7 @@ class AppUserAdmin(admin.ModelAdmin):
"open_in_stripe",
"personal_workspace",
]
autocomplete_fields = ["handle", "subscription"]
autocomplete_fields = ["subscription"]
inlines = [WorkspaceMembershipInline]

@admin.display(description="User Runs")
Expand Down
17 changes: 17 additions & 0 deletions app_users/migrations/0025_remove_appuser_handle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.3 on 2025-01-28 15:11

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('app_users', '0024_alter_appuser_handle'),
]

operations = [
migrations.RemoveField(
model_name='appuser',
name='handle',
),
]
19 changes: 4 additions & 15 deletions app_users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,6 @@ class AppUser(models.Model):

disable_safety_checker = models.BooleanField(default=False)

handle = models.OneToOneField(
"handles.Handle",
on_delete=models.SET_NULL,
default=None,
blank=True,
null=True,
related_name="user",
help_text="[deprecated] use workspace.handle instead",
)

banner_url = CustomURLField(blank=True, default="")
bio = StrippedTextField(blank=True, default="")
company = models.CharField(max_length=255, blank=True, default="")
Expand Down Expand Up @@ -229,9 +219,10 @@ def copy_from_firebase_user(self, user: auth.UserRecord) -> "AppUser":
self.save()
workspace, _ = self.get_or_create_personal_workspace()

if handle := Handle.create_default_for_user(user=self):
workspace.handle = handle
workspace.save()
if not self.is_anonymous:
if handle := Handle.create_default_for_workspace(workspace):
workspace.handle = handle
workspace.save()

return self

Expand All @@ -251,8 +242,6 @@ def cached_workspaces(self) -> list["Workspace"]:
) 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

Expand Down
3 changes: 1 addition & 2 deletions bots/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ def help_text(self, workspace: typing.Optional["Workspace"] = None):
case PublishedRunVisibility.UNLISTED:
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 handle := (workspace.handle or user.handle):
if handle := workspace.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>'
Expand Down
5 changes: 2 additions & 3 deletions daras_ai_v2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,9 +1502,8 @@ def render_workspace_author(
name = workspace.created_by.display_name
else:
name = workspace.display_name()
if show_as_link and workspace.is_personal:
handle = workspace.handle or workspace.created_by.handle
link = handle and handle.get_app_url()
if show_as_link and workspace.is_personal and workspace.handle_id:
link = workspace.handle.get_app_url()
else:
link = None
return cls._render_author(
Expand Down
48 changes: 14 additions & 34 deletions daras_ai_v2/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,7 @@ def edit_user_profile_page(workspace: "Workspace"):


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

gui.write("# Update your Profile")

Expand Down Expand Up @@ -445,16 +444,15 @@ def _edit_user_profile_photo_section(workspace: "Workspace"):

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 new_handle := gui.text_input(
"Username",
value=current_handle and current_handle.name or "",
value=workspace.handle and workspace.handle.name or "",
style=handle_style,
):
if not current_handle or current_handle.name != new_handle:
if not workspace.handle or workspace.handle.name != new_handle:
try:
Handle(name=new_handle).full_clean()
except ValidationError as e:
Expand All @@ -481,8 +479,6 @@ def _edit_user_profile_form_section(workspace: "Workspace"):
user.full_clean()
except ValidationError as e:
error_msg = "\n\n".join(e.messages)

if error_msg:
gui.error(error_msg, icon="⚠️")

if gui.button(
Expand All @@ -492,39 +488,23 @@ def _edit_user_profile_form_section(workspace: "Workspace"):
):
try:
with transaction.atomic():
if new_handle and not current_handle:
if new_handle and not workspace.handle:
# user adds a new 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
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
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()
elif new_handle and workspace.handle.name != new_handle:
# change existing handle
workspace.handle.name = new_handle
workspace.handle.save()
elif not new_handle and workspace.handle:
# remove existing handle
workspace.handle.delete()
workspace.handle = None
workspace.full_clean()
user.save()
workspace.save()
except (ValidationError, IntegrityError) as e:
for m in e.messages:
gui.error(m, icon="⚠️")
except ValidationError as e:
gui.error("\n\n".join(e.messages))
else:
gui.success("Changes saved")

Expand Down
12 changes: 4 additions & 8 deletions handles/admin.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
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]
+ [f"workspace__{field}" for field in WorkspaceAdmin.search_fields]
)
readonly_fields = ["user", "workspace", "created_at", "updated_at"]
search_fields = ["name", "redirect_url"] + [
f"workspace__{field}" for field in WorkspaceAdmin.search_fields
]
readonly_fields = ["workspace", "created_at", "updated_at"]

list_filter = [
("user", admin.EmptyFieldListFilter),
("workspace", admin.EmptyFieldListFilter),
("redirect_url", admin.EmptyFieldListFilter),
]
52 changes: 23 additions & 29 deletions handles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
import warnings
import typing

from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, RegexValidator
Expand All @@ -12,6 +13,11 @@
from bots.custom_fields import CustomURLField
from daras_ai_v2 import settings

if typing.TYPE_CHECKING:
from app_users.models import AppUser
from workspaces.models import Workspace


HANDLE_ALLOWED_CHARS = r"[A-Za-z0-9_\.-]+"
HANDLE_REGEX = rf"^{HANDLE_ALLOWED_CHARS}$"
HANDLE_MAX_LENGTH = 40
Expand Down Expand Up @@ -120,19 +126,7 @@ def __str__(self):
return f"@{self.name}"

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

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

Expand All @@ -144,16 +138,6 @@ def save(self, *args, **kwargs):
self.full_clean()
super().save(*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:
Expand All @@ -168,8 +152,8 @@ def has_redirect(self):
return bool(self.redirect_url)

@classmethod
def create_default_for_user(cls, user: "AppUser"):
for handle_name in _generate_handle_options(user):
def create_default_for_workspace(cls, workspace: "Workspace"):
for handle_name in _generate_handle_options(workspace):
if handle := _attempt_create_handle(handle_name):
return handle
return None
Expand All @@ -192,7 +176,19 @@ def _make_handle_from(name):
return name


def _generate_handle_options(user):
def _generate_handle_options(workspace: "Workspace") -> typing.Iterator[str]:
if workspace.is_personal:
yield from _generate_handle_options_for_personal_workspace(workspace.created_by)
else:
handle_name = _make_handle_from(workspace.display_name())
yield handle_name[:HANDLE_MAX_LENGTH]
for i in range(1, 10):
yield f"{handle_name[:HANDLE_MAX_LENGTH-1]}{i}"


def _generate_handle_options_for_personal_workspace(
user: "AppUser",
) -> typing.Iterator[str]:
if user.is_anonymous or not user.email:
return

Expand Down Expand Up @@ -234,9 +230,7 @@ def _generate_handle_options(user):
yield f"{email_handle[:HANDLE_MAX_LENGTH-1]}{i}"


def _attempt_create_handle(handle_name):
from handles.models import Handle

def _attempt_create_handle(handle_name: str):
handle = Handle(name=handle_name)
try:
handle.full_clean()
Expand Down
6 changes: 4 additions & 2 deletions handles/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def create_default_handle_by_name_and_email(name, email):
is_anonymous=False,
uid=get_random_doc_id(),
)
return Handle.create_default_for_user(user)
workspace, _ = user.get_or_create_personal_workspace()
return Handle.create_default_for_workspace(workspace)


def test_default_handle_when_user_is_anonymous(transactional_db):
Expand All @@ -24,7 +25,8 @@ def test_default_handle_when_user_is_anonymous(transactional_db):
is_anonymous=True,
uid=get_random_doc_id(),
)
handle = Handle.create_default_for_user(user)
workspace, _ = user.get_or_create_personal_workspace()
handle = Handle.create_default_for_workspace(workspace)
assert handle is None


Expand Down
2 changes: 1 addition & 1 deletion routers/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def _render_run(pr: PublishedRun):
return

if workspace.is_personal:
if handle := (workspace.handle or request.user.handle):
if handle := workspace.handle:
gui.caption(
f"""
All your Saved workflows are here, with public ones listed on your \
Expand Down
2 changes: 0 additions & 2 deletions routers/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,6 @@ def render_handle_page(request: Request, name: str):
handle = Handle.objects.get_by_name(name)
if handle.has_workspace and handle.workspace.is_personal:
user = handle.workspace.created_by
elif handle.has_user:
user = handle.user
else:
user = None

Expand Down
5 changes: 3 additions & 2 deletions scripts/create_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ def export_pr(pr: PublishedRun):

def export_workspace(workspace: Workspace):
yield from export_user(workspace.created_by)
yield export(workspace, include_fks={"created_by"})
if workspace.handle_id:
yield export(workspace.handle)
yield export(workspace, include_fks={"created_by", "handle"})


def export_user(user: AppUser):
yield user.handle
yield export(
user,
only_include={
Expand Down
2 changes: 1 addition & 1 deletion scripts/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -ex

echo "==> Downloading fixture.json..."
wget -N -nv https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/2f4f3e7c-e3bf-11ef-b200-02420a0001cf/fixture.json
wget -N -nv https://storage.googleapis.com/dara-c1b52.appspot.com/daras_ai/media/6cfdb56a-ed37-11ef-9b59-02420a000160/fixture.json

echo "==> Linting with black..."
black --check --diff .
Expand Down
Loading

0 comments on commit 76b80be

Please sign in to comment.