edit nickname/icon/etc
This commit is contained in:
parent
21960224c1
commit
cdc77b9cee
17 changed files with 267 additions and 46 deletions
4
.github/workflows/docker-dev.yml
vendored
4
.github/workflows/docker-dev.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
19
common/templates/_field.html
Normal file
19
common/templates/_field.html
Normal 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>
|
|
@ -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"]:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;'
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
84
users/profile.py
Normal 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"))
|
|
@ -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>
|
||||||
|
|
|
@ -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="已关注,点击可取消关注"
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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=""):
|
||||||
|
|
Loading…
Add table
Reference in a new issue