edit nickname/icon/etc

This commit is contained in:
Your Name 2023-08-22 17:13:52 +00:00 committed by Henri Dickson
parent 21960224c1
commit cdc77b9cee
17 changed files with 267 additions and 46 deletions

View file

@ -4,8 +4,8 @@ on: [push, pull_request]
jobs: jobs:
push_to_docker_hub: push_to_docker_hub:
name: Push image to Docker Hub name: build image and push to Docker Hub
if: github.repository_owner == 'alphatownsman' if: github.repository_owner == 'neodb-social'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo

View file

@ -207,7 +207,6 @@ if DEBUG:
# NEODB_STATIC_ROOT is readonly in docker mode, so we give it a writable place # NEODB_STATIC_ROOT is readonly in docker mode, so we give it a writable place
SASS_PROCESSOR_ROOT = "/tmp" SASS_PROCESSOR_ROOT = "/tmp"
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
@ -222,10 +221,25 @@ SILENCED_SYSTEM_CHECKS = [
"fields.W344", # Required by takahe: identical table name in different database "fields.W344", # Required by takahe: identical table name in different database
] ]
TAKAHE_MEDIA_PREFIX = "/media/" TAKAHE_MEDIA_URL = os.environ.get("TAKAHE_MEDIA_URL", "/media/")
TAKAHE_MEDIA_ROOT = os.environ.get("TAKAHE_MEDIA_ROOT", "media")
MEDIA_URL = "/m/" MEDIA_URL = "/m/"
MEDIA_ROOT = os.environ.get("NEODB_MEDIA_ROOT", os.path.join(BASE_DIR, "media/")) MEDIA_ROOT = os.environ.get("NEODB_MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
STORAGES = { # TODO: support S3
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
"takahe": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {
"location": TAKAHE_MEDIA_ROOT,
"base_url": TAKAHE_MEDIA_URL,
},
},
}
SITE_DOMAIN = os.environ.get("NEODB_SITE_DOMAIN", "nicedb.org") SITE_DOMAIN = os.environ.get("NEODB_SITE_DOMAIN", "nicedb.org")
SITE_INFO = { SITE_INFO = {
"site_name": os.environ.get("NEODB_SITE_NAME", "NiceDB"), "site_name": os.environ.get("NEODB_SITE_NAME", "NiceDB"),

View file

@ -9,6 +9,7 @@ from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.http import require_http_methods
from common.config import PAGE_LINK_NUMBER from common.config import PAGE_LINK_NUMBER
from common.utils import PageLinksGenerator, get_uuid_or_404 from common.utils import PageLinksGenerator, get_uuid_or_404
@ -255,6 +256,7 @@ def reviews(request, item_path, item_uuid):
) )
@require_http_methods(["GET"])
def discover(request): def discover(request):
if request.method != "GET": if request.method != "GET":
raise BadRequest() raise BadRequest()

View file

@ -0,0 +1,19 @@
<fieldset>
<label for="{{ field.id_for_label }}">
{{ field.label }}
{% if field.field.required %}<small>(Required)</small>{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<small>
{{ field.help_text|safe|linebreaksbr }}
{% if field.field.required %}<small>(Required)</small>{% endif %}
</small>
{% endif %}
{{ field.errors }}
{% if field.field.widget.input_type == "file" and field.value %}
<img alt="{{ field.label }}"
style="max-height: 4em"
src="{{ field.value.url }}">
{% endif %}
</fieldset>

View file

@ -21,6 +21,7 @@ def current_user_relationship(context, target_identity: "APIdentity"):
) )
r = { r = {
"requesting": False, "requesting": False,
"requested": False,
"following": False, "following": False,
"muting": False, "muting": False,
"rejecting": False, "rejecting": False,
@ -33,6 +34,7 @@ def current_user_relationship(context, target_identity: "APIdentity"):
r["rejecting"] = True r["rejecting"] = True
else: else:
r["requesting"] = current_identity.is_requesting(target_identity) r["requesting"] = current_identity.is_requesting(target_identity)
r["requested"] = current_identity.is_requested(target_identity)
r["muting"] = current_identity.is_muting(target_identity) r["muting"] = current_identity.is_muting(target_identity)
r["following"] = current_identity.is_following(target_identity) r["following"] = current_identity.is_following(target_identity)
if r["following"]: if r["following"]:

View file

@ -34,7 +34,7 @@ x-shared:
NEODB_TYPESENSE_PORT: 8108 NEODB_TYPESENSE_PORT: 8108
NEODB_TYPESENSE_KEY: eggplant NEODB_TYPESENSE_KEY: eggplant
NEODB_FROM_EMAIL: no-reply@${NEODB_SITE_DOMAIN} NEODB_FROM_EMAIL: no-reply@${NEODB_SITE_DOMAIN}
NEODB_MEDIA_ROOT: /www/m/ NEODB_MEDIA_ROOT: /www/m
TAKAHE_DB_NAME: takahe TAKAHE_DB_NAME: takahe
TAKAHE_DB_USER: takahe TAKAHE_DB_USER: takahe
TAKAHE_DB_PASSWORD: aubergine TAKAHE_DB_PASSWORD: aubergine
@ -47,7 +47,7 @@ x-shared:
TAKAHE_DATABASE_SERVER: postgres://takahe:aubergine@takahe-db/takahe TAKAHE_DATABASE_SERVER: postgres://takahe:aubergine@takahe-db/takahe
TAKAHE_CACHES_DEFAULT: redis://redis:6379/0 TAKAHE_CACHES_DEFAULT: redis://redis:6379/0
TAKAHE_MEDIA_BACKEND: local://www/media/ TAKAHE_MEDIA_BACKEND: local://www/media/
TAKAHE_MEDIA_ROOT: /www/media/ TAKAHE_MEDIA_ROOT: /www/media
TAKAHE_USE_PROXY_HEADERS: true TAKAHE_USE_PROXY_HEADERS: true
TAKAHE_STATOR_CONCURRENCY: 4 TAKAHE_STATOR_CONCURRENCY: 4
TAKAHE_STATOR_CONCURRENCY_PER_MODEL: 2 TAKAHE_STATOR_CONCURRENCY_PER_MODEL: 2
@ -147,7 +147,7 @@ services:
<<: *neodb-service <<: *neodb-service
# ports: # ports:
# - "18000:8000" # - "18000:8000"
command: /neodb/.venv/bin/gunicorn boofilsic.wsgi -w ${NEODB_WEB_WORKER_NUM:-8} --preload -b 0.0.0.0:8000 command: /neodb/.venv/bin/gunicorn boofilsic.wsgi -w ${NEODB_WEB_WORKER_NUM:-8} --preload --max-requests 1000 -b 0.0.0.0:8000
healthcheck: healthcheck:
test: ['CMD', 'wget', '-qO/tmp/test', 'http://127.0.0.1:8000/nodeinfo/2.0/'] test: ['CMD', 'wget', '-qO/tmp/test', 'http://127.0.0.1:8000/nodeinfo/2.0/']
depends_on: depends_on:
@ -172,7 +172,7 @@ services:
<<: *neodb-service <<: *neodb-service
# ports: # ports:
# - "19000:8000" # - "19000:8000"
command: /takahe/.venv/bin/gunicorn --chdir /takahe takahe.wsgi -w ${TAKAHE_WEB_WORKER_NUM:-8} --preload -b 0.0.0.0:8000 command: /takahe/.venv/bin/gunicorn --chdir /takahe takahe.wsgi -w ${TAKAHE_WEB_WORKER_NUM:-8} --max-requests 1000 --preload -b 0.0.0.0:8000
healthcheck: healthcheck:
test: ['CMD', 'wget', '-qO/tmp/test', 'http://127.0.0.1:8000/nodeinfo/2.0/'] test: ['CMD', 'wget', '-qO/tmp/test', 'http://127.0.0.1:8000/nodeinfo/2.0/']
depends_on: depends_on:

View file

@ -1,3 +1,4 @@
#!/bin/sh #!/bin/sh
chown app:app /www/media /www/m
envsubst '${NEODB_WEB_SERVER} ${TAKAHE_WEB_SERVER}' < $NGINX_CONF > /etc/nginx/conf.d/neodb.conf envsubst '${NEODB_WEB_SERVER} ${TAKAHE_WEB_SERVER}' < $NGINX_CONF > /etc/nginx/conf.d/neodb.conf
nginx -g 'daemon off;' nginx -g 'daemon off;'

View file

@ -1,4 +1,5 @@
import datetime import datetime
import os
import re import re
import secrets import secrets
import ssl import ssl
@ -15,10 +16,12 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.asymmetric import padding, rsa
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.core.files.storage import FileSystemStorage
from django.db import models, transaction from django.db import models, transaction
from django.template.defaultfilters import linebreaks_filter from django.template.defaultfilters import linebreaks_filter
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from loguru import logger from loguru import logger
from lxml import etree from lxml import etree
@ -280,6 +283,24 @@ class Domain(models.Model):
return self.domain return self.domain
def upload_store():
return FileSystemStorage(
location=settings.TAKAHE_MEDIA_ROOT, base_url=settings.TAKAHE_MEDIA_URL
)
def upload_namer(prefix, instance, filename):
"""
Names uploaded images.
By default, obscures the original name with a random UUID.
"""
_, old_extension = os.path.splitext(filename)
new_filename = secrets.token_urlsafe(20)
now = timezone.now()
return f"{prefix}/{now.year}/{now.month}/{now.day}/{new_filename}{old_extension}"
class Identity(models.Model): class Identity(models.Model):
""" """
Represents both local and remote Fediverse identities (actors) Represents both local and remote Fediverse identities (actors)
@ -303,6 +324,8 @@ class Identity(models.Model):
# state = StateField(IdentityStates) # state = StateField(IdentityStates)
state = models.CharField(max_length=100, default="outdated") state = models.CharField(max_length=100, default="outdated")
state_changed = models.DateTimeField(auto_now_add=True) state_changed = models.DateTimeField(auto_now_add=True)
state_next_attempt = models.DateTimeField(blank=True, null=True)
state_locked_until = models.DateTimeField(null=True, blank=True, db_index=True)
local = models.BooleanField(db_index=True) local = models.BooleanField(db_index=True)
users = models.ManyToManyField( users = models.ManyToManyField(
@ -321,10 +344,12 @@ class Identity(models.Model):
related_name="identities", related_name="identities",
) )
name = models.CharField(max_length=500, blank=True, null=True) name = models.CharField(max_length=500, blank=True, null=True, verbose_name=_("昵称"))
summary = models.TextField(blank=True, null=True) summary = models.TextField(blank=True, null=True, verbose_name=_("简介"))
manually_approves_followers = models.BooleanField(blank=True, null=True) manually_approves_followers = models.BooleanField(
discoverable = models.BooleanField(default=True) default=False, verbose_name=_("手工审核关注者")
)
discoverable = models.BooleanField(default=True, verbose_name=_("允许被发现或推荐"))
profile_uri = models.CharField(max_length=500, blank=True, null=True) profile_uri = models.CharField(max_length=500, blank=True, null=True)
inbox_uri = models.CharField(max_length=500, blank=True, null=True) inbox_uri = models.CharField(max_length=500, blank=True, null=True)
@ -337,12 +362,19 @@ class Identity(models.Model):
featured_collection_uri = models.CharField(max_length=500, blank=True, null=True) featured_collection_uri = models.CharField(max_length=500, blank=True, null=True)
actor_type = models.CharField(max_length=100, default="person") actor_type = models.CharField(max_length=100, default="person")
# icon = models.ImageField( icon = models.ImageField(
# upload_to=partial(upload_namer, "profile_images"), blank=True, null=True upload_to=partial(upload_namer, "profile_images"),
# ) blank=True,
# image = models.ImageField( null=True,
# upload_to=partial(upload_namer, "background_images"), blank=True, null=True verbose_name=_("头像"),
# ) storage=upload_store,
)
image = models.ImageField(
upload_to=partial(upload_namer, "background_images"),
blank=True,
null=True,
storage=upload_store,
)
# Should be a list of {"name":..., "value":...} dicts # Should be a list of {"name":..., "value":...} dicts
metadata = models.JSONField(blank=True, null=True) metadata = models.JSONField(blank=True, null=True)
@ -1177,7 +1209,7 @@ class Emoji(models.Model):
def full_url(self, always_show=False) -> RelativeAbsoluteUrl: def full_url(self, always_show=False) -> RelativeAbsoluteUrl:
if self.is_usable or always_show: if self.is_usable or always_show:
if self.file: if self.file:
return AutoAbsoluteUrl(settings.TAKAHE_MEDIA_PREFIX + self.file.name) return AutoAbsoluteUrl(settings.TAKAHE_MEDIA_URL + self.file.name)
# return AutoAbsoluteUrl(self.file.url) # return AutoAbsoluteUrl(self.file.url)
elif self.remote_url: elif self.remote_url:
return ProxyAbsoluteUrl( return ProxyAbsoluteUrl(

View file

@ -551,3 +551,30 @@ class Takahe:
else: else:
child_queryset = child_queryset.unlisted(include_replies=True) child_queryset = child_queryset.unlisted(include_replies=True)
return child_queryset return child_queryset
@staticmethod
def html2txt(html: str) -> str:
if not html:
return ""
return FediverseHtmlParser(html).plain_text
@staticmethod
def txt2html(txt: str) -> str:
if not txt:
return ""
return FediverseHtmlParser(linebreaks_filter(txt)).html
@staticmethod
def update_state(obj, state):
obj.state = state
obj.state_changed = timezone.now()
obj.state_next_attempt = None
obj.state_locked_until = None
obj.save(
update_fields=[
"state",
"state_changed",
"state_next_attempt",
"state_locked_until",
]
)

View file

@ -68,18 +68,6 @@ def data(request):
) )
@mastodon_request_included
@login_required
def account_info(request):
return render(
request,
"users/account.html",
{
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
},
)
@login_required @login_required
def data_import_status(request): def data_import_status(request):
return render( return render(

View file

@ -79,7 +79,11 @@ class APIdentity(models.Model):
@property @property
def avatar(self): def avatar(self):
if self.local: if self.local:
return self.takahe_identity.icon_uri or static("img/avatar.svg") return (
self.takahe_identity.icon.url
if self.takahe_identity.icon
else static("img/avatar.svg")
)
else: else:
return f"/proxy/identity_icon/{self.pk}/" return f"/proxy/identity_icon/{self.pk}/"
@ -134,6 +138,10 @@ class APIdentity(models.Model):
def blocking_identities(self): def blocking_identities(self):
return APIdentity.objects.filter(pk__in=self.blocking) return APIdentity.objects.filter(pk__in=self.blocking)
@property
def requested_follower_identities(self):
return APIdentity.objects.filter(pk__in=self.requested_followers)
@property @property
def follow_requesting_identities(self): def follow_requesting_identities(self):
return APIdentity.objects.filter(pk__in=self.following_request) return APIdentity.objects.filter(pk__in=self.following_request)
@ -161,10 +169,10 @@ class APIdentity(models.Model):
return Takahe.get_following_request_ids(self.pk) return Takahe.get_following_request_ids(self.pk)
def accept_follow_request(self, target: "APIdentity"): def accept_follow_request(self, target: "APIdentity"):
Takahe.accept_follow_request(self.pk, target.pk) Takahe.accept_follow_request(target.pk, self.pk)
def reject_follow_request(self, target: "APIdentity"): def reject_follow_request(self, target: "APIdentity"):
Takahe.reject_follow_request(self.pk, target.pk) Takahe.reject_follow_request(target.pk, self.pk)
def block(self, target: "APIdentity"): def block(self, target: "APIdentity"):
Takahe.block(self.pk, target.pk) Takahe.block(self.pk, target.pk)
@ -198,6 +206,9 @@ class APIdentity(models.Model):
def is_requesting(self, target: "APIdentity"): def is_requesting(self, target: "APIdentity"):
return target.pk in self.following_request return target.pk in self.following_request
def is_requested(self, target: "APIdentity"):
return target.pk in self.requested_followers
def is_followed_by(self, target: "APIdentity"): def is_followed_by(self, target: "APIdentity"):
return target.is_following(self) return target.is_following(self)

View file

@ -186,14 +186,7 @@ class User(AbstractUser):
@property @property
def avatar(self): def avatar(self):
if self.mastodon_account: return self.identity.avatar if self.identity else static("img/avatar.svg")
return self.mastodon_account.get("avatar") or static("img/avatar.svg")
if self.email:
return (
"https://www.gravatar.com/avatar/"
+ hashlib.md5(self.email.lower().encode()).hexdigest()
)
return static("img/avatar.svg")
@property @property
def handler(self): def handler(self):

84
users/profile.py Normal file
View file

@ -0,0 +1,84 @@
from datetime import timedelta
from typing import Any, Dict
from urllib.parse import quote
import django_rq
from django import forms
from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.core.exceptions import BadRequest, ObjectDoesNotExist
from django.core.mail import send_mail
from django.core.signing import TimestampSigner
from django.core.validators import EmailValidator
from django.db.models import Count, Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from loguru import logger
from common.config import *
from common.utils import AuthedHttpRequest
from journal.exporters.doufen import export_marks_task
from journal.importers.douban import DoubanImporter
from journal.importers.goodreads import GoodreadsImporter
from journal.importers.opml import OPMLImporter
from journal.models import remove_data_by_user, reset_journal_visibility_for_user
from mastodon import mastodon_request_included
from mastodon.api import *
from mastodon.api import verify_account
from social.models import reset_social_visibility_for_user
from takahe.models import Identity as TakaheIdentity
from takahe.utils import Takahe
from .models import Preference, User
from .tasks import *
class ProfileForm(forms.ModelForm):
class Meta:
model = TakaheIdentity
fields = [
"name",
"summary",
"manually_approves_followers",
"discoverable",
"icon",
]
def clean_summary(self):
return Takahe.txt2html(self.cleaned_data["summary"])
@login_required
def account_info(request):
profile_form = ProfileForm(
instance=request.user.identity.takahe_identity,
initial={
"summary": Takahe.html2txt(request.user.identity.summary),
},
)
return render(
request,
"users/account.html",
{
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
"profile_form": profile_form,
},
)
@login_required
def account_profile(request):
if request.method == "POST":
form = ProfileForm(
request.POST, request.FILES, instance=request.user.identity.takahe_identity
)
if form.is_valid():
i = form.save()
Takahe.update_state(i, "edited")
return HttpResponseRedirect(reverse("users:info"))

View file

@ -19,7 +19,7 @@
<div class="grid__main"> <div class="grid__main">
{% if allow_any_site %} {% if allow_any_site %}
<article> <article>
<details open> <details>
<summary>{% trans '用户名、电子邮件与社交身份' %}</summary> <summary>{% trans '用户名、电子邮件与社交身份' %}</summary>
<form action="{% url 'users:register' %}?next={{ request.path }}" <form action="{% url 'users:register' %}?next={{ request.path }}"
method="post"> method="post">
@ -91,6 +91,22 @@
</details> </details>
</article> </article>
{% endif %} {% endif %}
<article>
<details>
<summary>昵称、头像与其它个人信息</summary>
<form action="{% url 'users:profile' %}?next={{ request.path }}"
method="post"
enctype="multipart/form-data">
{% include "_field.html" with field=profile_form.name %}
{% include "_field.html" with field=profile_form.summary %}
{% include "_field.html" with field=profile_form.icon %}
{% include "_field.html" with field=profile_form.discoverable %}
{% include "_field.html" with field=profile_form.manually_approves_followers %}
{% csrf_token %}
<input type="submit" value="{% trans '保存' %}" id="save">
</form>
</details>
</article>
<article> <article>
<details> <details>
<summary>{% trans '正在关注的用户' %}</summary> <summary>{% trans '正在关注的用户' %}</summary>
@ -106,7 +122,7 @@
<article> <article>
<details> <details>
<summary>{% trans '请求关注你的用户' %}</summary> <summary>{% trans '请求关注你的用户' %}</summary>
{% include 'users/relationship_list.html' with name="请求关注者" id="follow_request" list=request.user.identity.follow_requesting_identities.all %} {% include 'users/relationship_list.html' with name="请求关注者" id="follow_request" list=request.user.identity.requested_follower_identities.all %}
</details> </details>
</article> </article>
<article> <article>

View file

@ -42,6 +42,26 @@
</a> </a>
</span> </span>
{% endif %} {% endif %}
{% if relationship.requested %}
<span>
<a title="接受关注请求"
class="activated"
hx-post="{% url 'users:accept_follow_request' identity.handler %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-check"></i>
</a>
</span>
<span>
<a title="拒绝关注请求"
class="activated"
hx-post="{% url 'users:reject_follow_request' identity.handler %}"
hx-target="closest .action"
hx-swap="innerHTML">
<i class="fa-solid fa-xmark"></i>
</a>
</span>
{% endif %}
{% if relationship.following %} {% if relationship.following %}
<span> <span>
<a title="已关注,点击可取消关注" <a title="已关注,点击可取消关注"

View file

@ -15,6 +15,7 @@ urlpatterns = [
path("fetch_refresh", fetch_refresh, name="fetch_refresh"), path("fetch_refresh", fetch_refresh, name="fetch_refresh"),
path("data", data, name="data"), path("data", data, name="data"),
path("info", account_info, name="info"), path("info", account_info, name="info"),
path("profile", account_profile, name="profile"),
path("data/import/status", data_import_status, name="import_status"), path("data/import/status", data_import_status, name="import_status"),
path("data/import/goodreads", import_goodreads, name="import_goodreads"), path("data/import/goodreads", import_goodreads, name="import_goodreads"),
path("data/import/douban", import_douban, name="import_douban"), path("data/import/douban", import_douban, name="import_douban"),
@ -29,6 +30,16 @@ urlpatterns = [
path("layout", set_layout, name="set_layout"), path("layout", set_layout, name="set_layout"),
path("follow/<str:user_name>", follow, name="follow"), path("follow/<str:user_name>", follow, name="follow"),
path("unfollow/<str:user_name>", unfollow, name="unfollow"), path("unfollow/<str:user_name>", unfollow, name="unfollow"),
path(
"accept_follow_request/<str:user_name>",
accept_follow_request,
name="accept_follow_request",
),
path(
"reject_follow_request/<str:user_name>",
reject_follow_request,
name="reject_follow_request",
),
path("mute/<str:user_name>", mute, name="mute"), path("mute/<str:user_name>", mute, name="mute"),
path("unmute/<str:user_name>", unmute, name="unmute"), path("unmute/<str:user_name>", unmute, name="unmute"),
path("block/<str:user_name>", block, name="block"), path("block/<str:user_name>", block, name="block"),

View file

@ -22,6 +22,7 @@ from .account import *
from .data import * from .data import *
from .forms import ReportForm from .forms import ReportForm
from .models import APIdentity, Preference, Report, User from .models import APIdentity, Preference, Report, User
from .profile import account_info, account_profile
def render_user_not_found(request, user_name=""): def render_user_not_found(request, user_name=""):