-
Notifications
You must be signed in to change notification settings - Fork 2
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
base: master
Are you sure you want to change the base?
Changes from 5 commits
bd355fa
b989d96
d876cca
f046ce6
a8d1662
760a5ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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="") | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for new users, handle should be on the workspace |
||
|
||
return self | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cache |
||
|
||
def get_anonymous_token(self): | ||
return auth.create_custom_token(self.uid).decode() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not using |
||
link = handle and handle.get_app_url() | ||
else: | ||
link = None | ||
return cls._render_author( | ||
|
@@ -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( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,9 @@ | |
) | ||
from handles.models import Handle | ||
|
||
if typing.TYPE_CHECKING: | ||
from workspaces.models import Workspace | ||
|
||
|
||
class ContributionsSummary(typing.NamedTuple): | ||
total: int | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
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 | ||
|
@@ -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"): | ||
|
@@ -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"): | ||
|
@@ -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") | ||
|
@@ -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")) | ||
|
||
|
@@ -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( | ||
|
@@ -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: | ||
|
@@ -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="⚠️") | ||
|
@@ -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]}" | ||
|
||
|
@@ -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) | ||
|
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), | ||
] |
There was a problem hiding this comment.
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