streamline profile url

This commit is contained in:
Your Name 2024-02-25 23:04:50 -05:00 committed by Henri Dickson
parent d45d1b3946
commit df0908b6bd
24 changed files with 141 additions and 92 deletions

View file

@ -58,7 +58,7 @@
{% if comment.rating_grade %}{{ comment.rating_grade|rating_star }}{% endif %}
<a href="{{ comment.owner.url }}"
class="nickname"
title="@{{ comment.owner.handler }}">{{ comment.owner.display_name }}</a>
title="@{{ comment.owner.handle }}">{{ comment.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">

View file

@ -62,7 +62,7 @@
{% if comment.rating_grade %}{{ comment.rating_grade|rating_star }}{% endif %}
<a href="{{ comment.owner.url }}"
class="nickname"
title="@{{ comment.owner.handler }}">{{ comment.owner.display_name }}</a>
title="@{{ comment.owner.handle }}">{{ comment.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">

View file

@ -22,7 +22,7 @@
{% if review.rating_grade %}{{ review.rating_grade|rating_star }}{% endif %}
<a href="{{ review.owner.url }}"
class="nickname"
title="@{{ review.owner.handler }}">{{ review.owner.display_name }}</a>
title="@{{ review.owner.handle }}">{{ review.owner.display_name }}</a>
</span>
<span class="action inline">
<span class="timestamp">{{ review.created_time|date }}</span>

View file

@ -43,7 +43,7 @@
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>
<div>
<a href="{{ mark.owner.url }}" title="@{{ mark.owner.handler }}">{{ mark.owner.display_name }}</a>
<a href="{{ mark.owner.url }}" title="@{{ mark.owner.handle }}">{{ mark.owner.display_name }}</a>
<span>{{ mark.action_label }}</span>
{% if mark.rating_grade %}{{ mark.rating_grade|rating_star }}{% endif %}
{% if mark.comment.item != item %}<a href="{{ mark.comment.item_url }}">{{ mark.comment.item.title }}</a>{% endif %}

View file

@ -45,7 +45,7 @@
<span>
<a href="{{ review.owner.url }}"
class="nickname"
title="@{{ review.owner.handler }}">{{ review.owner.display_name }}</a>
title="@{{ review.owner.handle }}">{{ review.owner.display_name }}</a>
</span>
</div>
<div>{{ review.plain_content | truncate:200 }}</div>

View file

@ -196,7 +196,7 @@
<div class="tag-list">
{% for t in top_tags %}
<span>
<a href="{% url 'journal:user_tag_member_list' identity.handler t %}">{{ t }}</a>
<a href="{% url 'journal:user_tag_member_list' identity.user.handler t %}">{{ t }}</a>
</span>
{% empty %}
<div class="empty">暂无可见标签</div>
@ -204,7 +204,7 @@
</div>
<small>
{% if top_tags %}
<a href="{% url 'journal:user_tag_list' identity.handler %}">...{% trans '全部' %}</a>
<a href="{% url 'journal:user_tag_list' identity.user.handler %}">...{% trans '全部' %}</a>
{% endif %}
</small>
</details>

View file

@ -1,9 +1,7 @@
import functools
import re
import uuid
from typing import TYPE_CHECKING
from django.db.models import query
from django.http import Http404, HttpRequest, HttpResponseRedirect, QueryDict
from django.utils import timezone
from django.utils.baseconv import base62
@ -55,7 +53,38 @@ def target_identity_required(func):
from users.views import render_user_blocked, render_user_not_found
try:
target = APIdentity.get_by_handler(user_name)
target = APIdentity.get_by_handle(user_name)
except APIdentity.DoesNotExist:
return render_user_not_found(request)
target_user = target.user
viewer = None
if target_user and not target_user.is_active:
return render_user_not_found(request)
if request.user.is_authenticated:
try:
viewer = APIdentity.objects.get(user=request.user)
except APIdentity.DoesNotExist:
return HttpResponseRedirect("/account/register")
if request.user != target_user:
if target.is_blocking(viewer) or target.is_blocked_by(viewer):
return render_user_blocked(request)
else:
viewer = None
request.target_identity = target
request.identity = viewer
return func(request, user_name, *args, **kwargs)
return wrapper
def profile_identity_required(func):
@functools.wraps(func)
def wrapper(request, user_name, *args, **kwargs):
from users.models import APIdentity
from users.views import render_user_blocked, render_user_not_found
try:
target = APIdentity.get_by_handle(user_name, match_linked=True)
except APIdentity.DoesNotExist:
return render_user_not_found(request)
target_user = target.user

View file

@ -10,7 +10,7 @@
{% csrf_token %}
{% if not application.is_official %}
<p>
<b>{{ application.name }}</b> 是由 <a href="{{ application.user.identity.url }}">@{{ application.user.identity.handler }}</a> 创建和维护的应用程序。
<b>{{ application.name }}</b> 是由 <a href="{{ application.user.identity.url }}">@{{ application.user.identity.handle }}</a> 创建和维护的应用程序。
{{ site_name }}无法保证其安全性和有效性,请自行验证确认后再授权。
</p>
{% endif %}

View file

@ -49,7 +49,7 @@
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>
<div>
{% comment %} <a href="{{mark.owner.url }}" title="@{{ mark.owner.handler }}">{{ mark.owner.display_name }}</a> {% endcomment %}
{% comment %} <a href="{{mark.owner.url }}" title="@{{ mark.owner.handle }}">{{ mark.owner.display_name }}</a> {% endcomment %}
<span>{{ mark.action_label }}</span>
{% if mark.rating_grade %}{{ mark.rating_grade|rating_star }}{% endif %}
</div>

View file

@ -62,7 +62,7 @@
<div class="info">
<p>
<a href="{{ collection.owner.url }}">{{ collection.owner.mastodon_account.display_name }}</a>
<span class="handler">@{{ collection.owner.handler }}</span>
<span class="handler">@{{ collection.owner.handle }}</span>
</p>
<p>
{% for cat, count in collection.get_summary.items %}

View file

@ -10,21 +10,21 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if identity.user == request.user %}
{% if me %}
<title>{{ site_name }} - {% trans '我的个人主页' %}</title>
{% else %}
<title>{{ site_name }} - {{ identity.display_name }}</title>
{% endif %}
<meta property="og:title"
content="{{ identity.handler }} - {{ site_name }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
content="{{ site_name }} - @{{ identity.handle }}">
<meta property="og:url" content="{{ identity.url }}">
<meta property="og:image" content="{{ identity.avatar }}">
<meta property="og:site_name" content="{{ site_name }}">
{% if not identity.anonymous_viewable %}<meta name="robots" content="noindex">{% endif %}
<link rel="alternate"
type="application/rss+xml"
title="{{ site_name }} - {{ identity.handler }}的评论"
href="{{ request.build_absolute_uri }}feed/reviews/">
title="{{ site_name }} - @{{ identity.handle }}的评论"
href="{{ identity.url }}/feed/reviews/">
{% include "common_libs.html" %}
<script src="{% static 'js/calendar_yearview_blocks.js' %}" defer></script>
<link href="{% static 'css/calendar_yearview_blocks.css' %}"
@ -68,7 +68,7 @@
</p>
</div>
<span class="calendar_data"
hx-get="{% url 'journal:user_calendar_data' identity.handler %}"
hx-get="{% url 'journal:user_calendar_data' identity.handle %}"
hx-trigger="load"
hx-swap="innerHTML"
style="display:none"></span>
@ -94,7 +94,7 @@
<h5>
{{ shelf.title }}
<small>
<a href="{% if shelf_type == 'reviewed' %}{% url 'journal:user_review_list' identity.handler category %}{% else %}{% url 'journal:user_mark_list' identity.handler shelf_type category %}{% endif %}">{{ shelf.count }}</a>
<a href="{% if shelf_type == 'reviewed' %}{% url 'journal:user_review_list' identity.handle category %}{% else %}{% url 'journal:user_mark_list' identity.handle shelf_type category %}{% endif %}">{{ shelf.count }}</a>
</small>
</h5>
<ul class="cards">
@ -133,7 +133,7 @@
<h5>
{% trans '创建的收藏单' %}
<small>
<a href="{% url 'journal:user_collection_list' identity.handler %}">{{ collections_count }}</a>
<a href="{% url 'journal:user_collection_list' identity.handle %}">{{ collections_count }}</a>
{% if identity.user == request.user %}
<a href="{% url 'journal:collection_create' %}">
<i class="fa-regular fa-square-plus"></i>
@ -173,7 +173,7 @@
<h5>
{% trans '喜欢的收藏单' %}
<small>
<a href="{% url 'journal:user_liked_collection_list' identity.handler %}">{{ liked_collections_count }}</a>
<a href="{% url 'journal:user_liked_collection_list' identity.handle %}">{{ liked_collections_count }}</a>
</small>
</h5>
<ul class="cards">

View file

@ -50,7 +50,7 @@
</div>
<div class="info">
<a href="{{ review.owner.url }}">{{ review.owner.display_name }}</a>
<span class="handler">@{{ review.owner.handler }}</span>
<span class="handler">@{{ review.owner.handle }}</span>
<br>
评论
<a href="{{ review.item.url }}">{{ review.item.title }}</a>

View file

@ -40,7 +40,7 @@
{% if liked %}
-
<a href="{{ collection.owner.url }}"
title="@{{ collection.owner.handler }}">{{ collection.owner.display_name }}</a>
title="@{{ collection.owner.handle }}">{{ collection.owner.display_name }}</a>
{% endif %}
</p>
{% empty %}

View file

@ -25,7 +25,7 @@
{% for v in tags %}
<span style="margin-right:2em; white-space: nowrap;">
<span>
<a href="{% url 'journal:user_tag_member_list' identity.handler v.title %}">{{ v.title }}</a>
<a href="{% url 'journal:user_tag_member_list' identity.handle v.title %}">{{ v.title }}</a>
</span>
<span>({{ v.total }})</span>
</span>

View file

@ -13,6 +13,7 @@ from common.utils import (
AuthedHttpRequest,
PageLinksGenerator,
get_uuid_or_404,
profile_identity_required,
target_identity_required,
)

View file

@ -1,32 +1,42 @@
import datetime
from django.contrib.auth.decorators import login_required
from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods
from user_messages import api as msg
from catalog.models import *
from common.utils import AuthedHttpRequest
from users.models import APIdentity, User
from users.views import render_user_blocked, render_user_not_found
from ..forms import *
from ..models import *
from .common import render_list, target_identity_required
from .common import profile_identity_required, target_identity_required
@require_http_methods(["GET"])
@target_identity_required
@profile_identity_required
def profile(request: AuthedHttpRequest, user_name):
target = request.target_identity
# if user.mastodon_acct != user_name and user.username != user_name:
# return redirect(user.url)
if not request.user.is_authenticated and not target.anonymous_viewable:
return render(request, "users/home_anonymous.html", {"user": target.user})
me = target.user == request.user
if not request.user.is_authenticated and (
not target.local or not target.anonymous_viewable
):
return render(
request,
"users/home_anonymous.html",
{"identity": target, "redir": f"/account/login?next={target.url}"},
)
if (target.local and user_name != target.handle) or (
not target.local and user_name != f"@{target.handle}"
):
return render(
request,
"users/home_anonymous.html",
{"identity": target, "redir": target.url},
)
me = target.local and target.user == request.user
qv = q_owned_piece_visible_to_user(request.user, target)
shelf_list = {}
@ -91,6 +101,7 @@ def profile(request: AuthedHttpRequest, user_name):
{
"user": target.user,
"identity": target,
"me": me,
"top_tags": top_tags,
"shelf_list": shelf_list,
"collections": collections[:10],

View file

@ -3,8 +3,8 @@ import mimetypes
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.syndication.views import Feed
from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.core.exceptions import BadRequest, PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
@ -106,7 +106,7 @@ MAX_ITEM_PER_TYPE = 10
class ReviewFeed(Feed):
def get_object(self, request, *args, **kwargs):
return APIdentity.get_by_handler(kwargs["username"])
return APIdentity.get_by_handle(kwargs["username"])
def title(self, owner):
return "%s的评论" % owner.display_name if owner else "无效链接"

View file

@ -90,7 +90,7 @@ class APIdentity(models.Model):
@property
def url(self):
return f"/users/{self.handler}/"
return f"/users/{self.handle}/" if self.local else f"/users/@{self.handle}/"
@property
def preference(self):
@ -101,11 +101,11 @@ class APIdentity(models.Model):
return f"{self.username}@{self.domain_name}"
@property
def handler(self):
def handle(self):
if self.local:
return self.username
else:
return f"@{self.username}@{self.domain_name}"
return f"{self.username}@{self.domain_name}"
@property
def following(self):
@ -214,11 +214,25 @@ class APIdentity(models.Model):
return Takahe.get_is_follow_requesting(target.pk, self.pk)
@classmethod
def get_by_handler(cls, handler: str) -> "APIdentity":
def get_remote(cls, username, domain):
i = cls.objects.filter(
username__iexact=username, domain_name__iexact=domain, deleted__isnull=True
).first()
if i:
return i
if domain != settings.SITE_INFO["site_domain"].lower():
identity = Takahe.get_identity_by_handler(username, domain)
if identity:
return Takahe.get_or_create_remote_apidentity(identity)
@classmethod
def get_by_handle(cls, handler: str, match_linked=False) -> "APIdentity":
"""
Handler format
'id' - local identity with username 'id'
'id@site' - local identity with linked mastodon id == 'id@site'
'id@site'
match_linked = True - local identity with linked mastodon id == 'id@site' (for backward compatibility)
match_linked = False - remote activitypub identity 'id@site'
'@id' - local identity with username 'id'
'@id@site' - remote activitypub identity 'id@site'
"""
@ -231,22 +245,22 @@ class APIdentity(models.Model):
deleted__isnull=True,
)
elif l == 2:
return cls.objects.get(
user__mastodon_username__iexact=s[0],
user__mastodon_site__iexact=s[1],
deleted__isnull=True,
)
if match_linked:
return cls.objects.get(
user__mastodon_username__iexact=s[0],
user__mastodon_site__iexact=s[1],
deleted__isnull=True,
)
else:
i = cls.get_remote(s[0], s[1])
if i:
return i
raise cls.DoesNotExist(f"Identity not found @{handler}")
elif l == 3 and s[0] == "":
i = cls.objects.filter(
username__iexact=s[1], domain_name__iexact=s[2], deleted__isnull=True
).first()
i = cls.get_remote(s[1], s[2])
if i:
return i
if s[2].lower() != settings.SITE_INFO["site_domain"].lower():
identity = Takahe.get_identity_by_handler(s[1], s[2])
if identity:
return Takahe.get_or_create_remote_apidentity(identity)
raise cls.DoesNotExist(f"Identity not exist {handler}")
raise cls.DoesNotExist(f"Identity not found {handler}")
else:
raise cls.DoesNotExist(f"Identity handler invalid {handler}")

View file

@ -4,21 +4,16 @@
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh"
content="0;URL={% url 'users:login' %}?next={{ request.path }}" />
<title>{{ site_name }} - {{ identity.handler }}</title>
<link rel="alternate"
type="application/rss+xml"
title="{{ site_name }} - {{ identity.handler }}的评论"
href="{{ request.build_absolute_uri }}feed/reviews/">
<meta http-equiv="refresh" content="0;URL={{ redir }}" />
<title>{{ site_name }} - {{ identity.handle }}</title>
<meta property="og:title"
content="{{ site_name }} - {{ identity.handler }}的主页">
content="{{ site_name }} - {{ identity.handle }}的主页">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:url" content="{{ identity.url }}">
<meta property="og:image" content="{{ identity.avatar }}">
</head>
<body>
{% if identity.user.mastodon_account.url %}
{% if identity.local and identity.user.mastodon_account.url %}
<a href="{{ identity.user.mastodon_account.url }}"
rel="me"
style="display:none">Mastodon verification</a>

View file

@ -47,7 +47,7 @@
</header>
<div>
{% if request.user.is_authenticated %}
<a href="{% url 'common:home' %}" class="button">{% trans '前往首页' %}</a>
<a href="{{ request.session.next_url | default:'/' }}" class="button">{% trans '前往首页' %}</a>
{% else %}
<form action="{% url 'users:connect' %}" method="post">
{% csrf_token %}

View file

@ -4,7 +4,7 @@
<span class="tag-list">
<span><a title="点击可取消屏蔽"
hx-confirm="确定要取消对该用户的屏蔽吗?"
hx-post="{% url 'users:unblock' identity.handler %}"
hx-post="{% url 'users:unblock' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">已屏蔽</a></span>
</span>
@ -46,7 +46,7 @@
<span>
<a title="接受关注请求"
class="activated"
hx-post="{% url 'users:accept_follow_request' identity.handler %}"
hx-post="{% url 'users:accept_follow_request' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-check"></i>
@ -55,7 +55,7 @@
<span>
<a title="拒绝关注请求"
class="activated"
hx-post="{% url 'users:reject_follow_request' identity.handler %}"
hx-post="{% url 'users:reject_follow_request' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-xmark"></i>
@ -66,7 +66,7 @@
<span>
<a title="已关注,点击可取消关注"
class="activated"
hx-post="{% url 'users:unfollow' identity.handler %}"
hx-post="{% url 'users:unfollow' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-user-check"></i>
@ -76,7 +76,7 @@
<span>
<a title="已发送关注请求,点击可取消"
class="activated"
hx-post="{% url 'users:unfollow' identity.handler %}"
hx-post="{% url 'users:unfollow' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-user-clock"></i>
@ -85,7 +85,7 @@
{% else %}
<span>
<a title="点击可关注该用户"
hx-post="{% url 'users:follow' identity.handler %}"
hx-post="{% url 'users:follow' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-user-plus"></i>
@ -95,7 +95,7 @@
{% if not relationship.muting %}
<span>
<a title="点击可隐藏该用户"
hx-post="{% url 'users:mute' identity.handler %}"
hx-post="{% url 'users:mute' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-eye"></i>
@ -105,7 +105,7 @@
<span>
<a title="已隐藏,点击可取消隐藏"
class="activated"
hx-post="{% url 'users:unmute' identity.handler %}"
hx-post="{% url 'users:unmute' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-eye-slash"></i>
@ -115,11 +115,10 @@
<span>
<a title="点击可屏蔽该用户"
hx-confirm="确定要屏蔽该用户吗?"
hx-post="{% url 'users:block' identity.handler %}"
hx-post="{% url 'users:block' identity.handle %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-user-slash"></i>
</a>
</span>
{% comment %} <span><a href="{% url 'users:report' %}?user_id={{ identity.id }}">{% trans '投诉用户' %}</a></span> {% endcomment %}
{% endif %}

View file

@ -4,7 +4,7 @@
<code class="{{ id }}_handler"
style="cursor:pointer"
onmouseleave="$(this).removeAttr('data-tooltip')"
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">{{ identity.handler }}</code>
onclick="navigator.clipboard.writeText(this.innerText);$(this).data('tooltip','copied');">@{{ identity.handle }}</code>
</p>
{% empty %}
<p class="empty">无数据</p>

View file

@ -19,17 +19,17 @@ class UserTest(TestCase):
self.domain = settings.SITE_INFO.get("site_domain")
def test_handle(self):
self.assertEqual(APIdentity.get_by_handler("Alice"), self.alice)
self.assertEqual(APIdentity.get_by_handler("@alice"), self.alice)
self.assertEqual(APIdentity.get_by_handler("Alice@MySpace"), self.alice)
self.assertEqual(APIdentity.get_by_handler("alice@myspace"), self.alice)
self.assertEqual(APIdentity.get_by_handler("@alice@" + self.domain), self.alice)
self.assertEqual(APIdentity.get_by_handler("@Alice@" + self.domain), self.alice)
self.assertEqual(APIdentity.get_by_handle("Alice"), self.alice)
self.assertEqual(APIdentity.get_by_handle("@alice"), self.alice)
self.assertEqual(APIdentity.get_by_handle("Alice@MySpace", True), self.alice)
self.assertEqual(APIdentity.get_by_handle("alice@myspace", True), self.alice)
self.assertEqual(APIdentity.get_by_handle("@alice@" + self.domain), self.alice)
self.assertEqual(APIdentity.get_by_handle("@Alice@" + self.domain), self.alice)
self.assertRaises(
APIdentity.DoesNotExist, APIdentity.get_by_handler, "@Alice@MySpace"
APIdentity.DoesNotExist, APIdentity.get_by_handle, "@Alice@MySpace"
)
self.assertRaises(
APIdentity.DoesNotExist, APIdentity.get_by_handler, "@alice@KKCity"
APIdentity.DoesNotExist, APIdentity.get_by_handle, "@alice@KKCity"
)
def test_fetch(self):

View file

@ -62,7 +62,7 @@ def render_user_noanonymous(request):
def query_identity(request, handle):
try:
i = APIdentity.get_by_handler(handle)
i = APIdentity.get_by_handle(handle)
return redirect(i.url)
except APIdentity.DoesNotExist:
if len(handle.split("@")) == 3:
@ -77,7 +77,7 @@ def query_identity(request, handle):
def fetch_refresh(request):
handle = request.GET.get("handle", "")
try:
i = APIdentity.get_by_handler(handle)
i = APIdentity.get_by_handle(handle)
return HTTPResponseHXRedirect(i.url)
except:
retry = int(request.GET.get("retry", 0)) + 1
@ -161,7 +161,7 @@ def unblock(request: AuthedHttpRequest, user_name):
if request.method != "POST":
raise BadRequest()
try:
target = APIdentity.get_by_handler(user_name)
target = APIdentity.get_by_handle(user_name)
except APIdentity.DoesNotExist:
return render_user_not_found(request)
target_user = target.user