Skip to content

Commit 2dfdaf9

Browse files
nikochikomilovate
authored andcommitted
remove deprecated appuser handles in favor of workspace handles (#591)
* 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
1 parent e8367fa commit 2dfdaf9

File tree

15 files changed

+93
-114
lines changed

15 files changed

+93
-114
lines changed

app_users/admin.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ class AppUserAdmin(admin.ModelAdmin):
5353
"Profile Options",
5454
{
5555
"fields": [
56-
"handle",
5756
"display_name",
5857
"bio",
5958
("company", "github_username"),
@@ -72,7 +71,6 @@ class AppUserAdmin(admin.ModelAdmin):
7271
]
7372
list_display = [
7473
"uid",
75-
"handle",
7674
"display_name",
7775
"email",
7876
"phone_number",
@@ -84,7 +82,6 @@ class AppUserAdmin(admin.ModelAdmin):
8482
"display_name",
8583
"email",
8684
"phone_number",
87-
"handle__name",
8885
]
8986
list_filter = [
9087
"is_anonymous",
@@ -108,7 +105,7 @@ class AppUserAdmin(admin.ModelAdmin):
108105
"open_in_stripe",
109106
"personal_workspace",
110107
]
111-
autocomplete_fields = ["handle", "subscription"]
108+
autocomplete_fields = ["subscription"]
112109
inlines = [WorkspaceMembershipInline]
113110

114111
@admin.display(description="User Runs")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.1.3 on 2025-01-28 15:11
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('app_users', '0024_alter_appuser_handle'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='appuser',
15+
name='handle',
16+
),
17+
]

app_users/models.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,6 @@ class AppUser(models.Model):
122122

123123
disable_safety_checker = models.BooleanField(default=False)
124124

125-
handle = models.OneToOneField(
126-
"handles.Handle",
127-
on_delete=models.SET_NULL,
128-
default=None,
129-
blank=True,
130-
null=True,
131-
related_name="user",
132-
help_text="[deprecated] use workspace.handle instead",
133-
)
134-
135125
banner_url = CustomURLField(blank=True, default="")
136126
bio = StrippedTextField(blank=True, default="")
137127
company = models.CharField(max_length=255, blank=True, default="")
@@ -229,9 +219,10 @@ def copy_from_firebase_user(self, user: auth.UserRecord) -> "AppUser":
229219
self.save()
230220
workspace, _ = self.get_or_create_personal_workspace()
231221

232-
if handle := Handle.create_default_for_user(user=self):
233-
workspace.handle = handle
234-
workspace.save()
222+
if not self.is_anonymous:
223+
if handle := Handle.create_default_for_workspace(workspace):
224+
workspace.handle = handle
225+
workspace.save()
235226

236227
return self
237228

@@ -251,8 +242,6 @@ def cached_workspaces(self) -> list["Workspace"]:
251242
) or [self.get_or_create_personal_workspace()[0]]
252243

253244
def get_handle(self) -> Handle | None:
254-
if self.handle:
255-
return self.handle
256245
workspace, _ = self.get_or_create_personal_workspace()
257246
return workspace.handle
258247

bots/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ def help_text(self, workspace: typing.Optional["Workspace"] = None):
6666
case PublishedRunVisibility.UNLISTED:
6767
return f"{self.get_icon()} Only me + people with a link"
6868
case PublishedRunVisibility.PUBLIC if workspace and workspace.is_personal:
69-
user = workspace.created_by
70-
if handle := (workspace.handle or user.handle):
69+
if handle := workspace.handle:
7170
profile_url = handle.get_app_url()
7271
pretty_profile_url = urls.remove_scheme(profile_url).rstrip("/")
7372
return f'{self.get_icon()} Public on <a href="{pretty_profile_url}" target="_blank">{profile_url}</a>'

daras_ai_v2/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,9 +1502,8 @@ def render_workspace_author(
15021502
name = workspace.created_by.display_name
15031503
else:
15041504
name = workspace.display_name()
1505-
if show_as_link and workspace.is_personal:
1506-
handle = workspace.handle or workspace.created_by.handle
1507-
link = handle and handle.get_app_url()
1505+
if show_as_link and workspace.is_personal and workspace.handle_id:
1506+
link = workspace.handle.get_app_url()
15081507
else:
15091508
link = None
15101509
return cls._render_author(

daras_ai_v2/profiles.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,7 @@ def edit_user_profile_page(workspace: "Workspace"):
275275

276276

277277
def _edit_user_profile_header(workspace: "Workspace"):
278-
user = workspace.created_by
279-
handle = workspace.handle or user.handle # TODO: remove fallback
278+
handle = workspace.handle
280279

281280
gui.write("# Update your Profile")
282281

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

446445
def _edit_user_profile_form_section(workspace: "Workspace"):
447446
user = workspace.created_by
448-
current_handle = workspace.handle or user.handle # TODO: remove fallback
449447
user.display_name = gui.text_input("Name", value=user.display_name)
450448

451449
handle_style: dict[str, str] = {}
452450
if new_handle := gui.text_input(
453451
"Username",
454-
value=current_handle and current_handle.name or "",
452+
value=workspace.handle and workspace.handle.name or "",
455453
style=handle_style,
456454
):
457-
if not current_handle or current_handle.name != new_handle:
455+
if not workspace.handle or workspace.handle.name != new_handle:
458456
try:
459457
Handle(name=new_handle).full_clean()
460458
except ValidationError as e:
@@ -481,8 +479,6 @@ def _edit_user_profile_form_section(workspace: "Workspace"):
481479
user.full_clean()
482480
except ValidationError as e:
483481
error_msg = "\n\n".join(e.messages)
484-
485-
if error_msg:
486482
gui.error(error_msg, icon="⚠️")
487483

488484
if gui.button(
@@ -492,39 +488,23 @@ def _edit_user_profile_form_section(workspace: "Workspace"):
492488
):
493489
try:
494490
with transaction.atomic():
495-
if new_handle and not current_handle:
491+
if new_handle and not workspace.handle:
496492
# user adds a new handle
497493
workspace.handle = Handle(name=new_handle)
498494
workspace.handle.save()
499-
elif (
500-
new_handle and current_handle and current_handle.name != new_handle
501-
):
502-
# user changes existing handle
503-
if workspace.handle:
504-
workspace.handle.name = new_handle
505-
workspace.handle.save()
506-
elif user.handle:
507-
# TODO: remove this once all handles are migrated
508-
user.handle.delete()
509-
user.handle = None
510-
workspace.handle = Handle(name=new_handle)
511-
workspace.handle.save()
512-
elif not new_handle and current_handle:
513-
# user removes existing handle
514-
if workspace.handle:
515-
workspace.handle.delete()
516-
workspace.handle = None
517-
elif user.handle:
518-
# TODO: remove this once all handles are migrated
519-
user.handle.delete()
520-
user.handle = None
521-
user.full_clean()
495+
elif new_handle and workspace.handle.name != new_handle:
496+
# change existing handle
497+
workspace.handle.name = new_handle
498+
workspace.handle.save()
499+
elif not new_handle and workspace.handle:
500+
# remove existing handle
501+
workspace.handle.delete()
502+
workspace.handle = None
522503
workspace.full_clean()
523504
user.save()
524505
workspace.save()
525-
except (ValidationError, IntegrityError) as e:
526-
for m in e.messages:
527-
gui.error(m, icon="⚠️")
506+
except ValidationError as e:
507+
gui.error("\n\n".join(e.messages))
528508
else:
529509
gui.success("Changes saved")
530510

handles/admin.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
from django.contrib import admin
22

3-
from app_users.admin import AppUserAdmin
43
from workspaces.admin import WorkspaceAdmin
54
from .models import Handle
65

76

87
@admin.register(Handle)
98
class HandleAdmin(admin.ModelAdmin):
10-
search_fields = (
11-
["name", "redirect_url"]
12-
+ [f"user__{field}" for field in AppUserAdmin.search_fields]
13-
+ [f"workspace__{field}" for field in WorkspaceAdmin.search_fields]
14-
)
15-
readonly_fields = ["user", "workspace", "created_at", "updated_at"]
9+
search_fields = ["name", "redirect_url"] + [
10+
f"workspace__{field}" for field in WorkspaceAdmin.search_fields
11+
]
12+
readonly_fields = ["workspace", "created_at", "updated_at"]
1613

1714
list_filter = [
18-
("user", admin.EmptyFieldListFilter),
1915
("workspace", admin.EmptyFieldListFilter),
2016
("redirect_url", admin.EmptyFieldListFilter),
2117
]

handles/models.py

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import warnings
5+
import typing
56

67
from django.core.exceptions import ValidationError
78
from django.core.validators import MaxLengthValidator, RegexValidator
@@ -12,6 +13,11 @@
1213
from bots.custom_fields import CustomURLField
1314
from daras_ai_v2 import settings
1415

16+
if typing.TYPE_CHECKING:
17+
from app_users.models import AppUser
18+
from workspaces.models import Workspace
19+
20+
1521
HANDLE_ALLOWED_CHARS = r"[A-Za-z0-9_\.-]+"
1622
HANDLE_REGEX = rf"^{HANDLE_ALLOWED_CHARS}$"
1723
HANDLE_MAX_LENGTH = 40
@@ -120,19 +126,7 @@ def __str__(self):
120126
return f"@{self.name}"
121127

122128
def _validate_exclusive(self):
123-
if (
124-
self.has_workspace
125-
and self.has_user
126-
and self.workspace.created_by_id == self.user.id
127-
):
128-
# TODO: remove this once all handles are migrated
129-
return
130-
131-
lookups = [
132-
self.has_redirect,
133-
self.has_workspace,
134-
self.has_user,
135-
]
129+
lookups = [self.has_redirect, self.has_workspace]
136130
if sum(lookups) > 1:
137131
raise ValidationError("A handle must be exclusive")
138132

@@ -144,16 +138,6 @@ def save(self, *args, **kwargs):
144138
self.full_clean()
145139
super().save(*args, **kwargs)
146140

147-
@property
148-
def has_user(self):
149-
warnings.warn("deprecated, use `has_workspace` instead", DeprecationWarning)
150-
try:
151-
self.user
152-
except Handle.user.RelatedObjectDoesNotExist:
153-
return False
154-
else:
155-
return True
156-
157141
@property
158142
def has_workspace(self):
159143
try:
@@ -168,8 +152,8 @@ def has_redirect(self):
168152
return bool(self.redirect_url)
169153

170154
@classmethod
171-
def create_default_for_user(cls, user: "AppUser"):
172-
for handle_name in _generate_handle_options(user):
155+
def create_default_for_workspace(cls, workspace: "Workspace"):
156+
for handle_name in _generate_handle_options(workspace):
173157
if handle := _attempt_create_handle(handle_name):
174158
return handle
175159
return None
@@ -192,7 +176,19 @@ def _make_handle_from(name):
192176
return name
193177

194178

195-
def _generate_handle_options(user):
179+
def _generate_handle_options(workspace: "Workspace") -> typing.Iterator[str]:
180+
if workspace.is_personal:
181+
yield from _generate_handle_options_for_personal_workspace(workspace.created_by)
182+
else:
183+
handle_name = _make_handle_from(workspace.display_name())
184+
yield handle_name[:HANDLE_MAX_LENGTH]
185+
for i in range(1, 10):
186+
yield f"{handle_name[:HANDLE_MAX_LENGTH-1]}{i}"
187+
188+
189+
def _generate_handle_options_for_personal_workspace(
190+
user: "AppUser",
191+
) -> typing.Iterator[str]:
196192
if user.is_anonymous or not user.email:
197193
return
198194

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

236232

237-
def _attempt_create_handle(handle_name):
238-
from handles.models import Handle
239-
233+
def _attempt_create_handle(handle_name: str):
240234
handle = Handle(name=handle_name)
241235
try:
242236
handle.full_clean()

handles/tests.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def create_default_handle_by_name_and_email(name, email):
1313
is_anonymous=False,
1414
uid=get_random_doc_id(),
1515
)
16-
return Handle.create_default_for_user(user)
16+
workspace, _ = user.get_or_create_personal_workspace()
17+
return Handle.create_default_for_workspace(workspace)
1718

1819

1920
def test_default_handle_when_user_is_anonymous(transactional_db):
@@ -24,7 +25,8 @@ def test_default_handle_when_user_is_anonymous(transactional_db):
2425
is_anonymous=True,
2526
uid=get_random_doc_id(),
2627
)
27-
handle = Handle.create_default_for_user(user)
28+
workspace, _ = user.get_or_create_personal_workspace()
29+
handle = Handle.create_default_for_workspace(workspace)
2830
assert handle is None
2931

3032

routers/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def _render_run(pr: PublishedRun):
329329
return
330330

331331
if workspace.is_personal:
332-
if handle := (workspace.handle or request.user.handle):
332+
if handle := workspace.handle:
333333
gui.caption(
334334
f"""
335335
All your Saved workflows are here, with public ones listed on your \

routers/root.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,8 +651,6 @@ def render_handle_page(request: Request, name: str):
651651
handle = Handle.objects.get_by_name(name)
652652
if handle.has_workspace and handle.workspace.is_personal:
653653
user = handle.workspace.created_by
654-
elif handle.has_user:
655-
user = handle.user
656654
else:
657655
user = None
658656

scripts/create_fixture.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ def export_pr(pr: PublishedRun):
115115

116116
def export_workspace(workspace: Workspace):
117117
yield from export_user(workspace.created_by)
118-
yield export(workspace, include_fks={"created_by"})
118+
if workspace.handle_id:
119+
yield export(workspace.handle)
120+
yield export(workspace, include_fks={"created_by", "handle"})
119121

120122

121123
def export_user(user: AppUser):
122-
yield user.handle
123124
yield export(
124125
user,
125126
only_include={

scripts/run-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
set -ex
44

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

88
echo "==> Linting with black..."
99
black --check --diff .

0 commit comments

Comments
 (0)