pin a hashtag

This commit is contained in:
Your Name 2024-06-04 16:51:51 -04:00 committed by Henri Dickson
parent a20779ce31
commit 432b435083
13 changed files with 313 additions and 154 deletions

View file

@ -78,11 +78,11 @@ dialog {
.grid>div:nth-child(2) fieldset {
float: unset;
}
.grid {
grid-row-gap: 0;
}
}
fieldset {
margin: 0;
}
article {
padding-bottom: 1em;

View file

@ -1,4 +1,4 @@
h1, h2, h3, h4, h5, h6 {
h1, h2, h3, h4, h5 {
text-transform: capitalize;
}

View file

@ -35,7 +35,7 @@
<img src="{{ identity.avatar|relative_uri }}" alt="">
</a>
</div>
<div>
<div style="align-content:center;">
<hgroup>
<h6 class="nickname">{{ identity.display_name }}</h6>
<div>
@ -68,9 +68,10 @@
{% include 'users/profile_actions.html' %}
{% endif %}
</span>
<p>
{{ identity.summary|bleach:"a,p,span,br"|default:"<br>" }}
</p>
<div>
{{ identity.summary|bleach:"a,p,span,br"|default:"" }}
<br>
</div>
</details>
</article>
</section>

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.13 on 2024-06-04 19:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("journal", "0024_i18n"),
]
operations = [
migrations.AddField(
model_name="tag",
name="pinned",
field=models.BooleanField(default=False, null=True),
),
]

View file

@ -47,8 +47,7 @@ class Tag(List):
title = models.CharField(
max_length=100, null=False, blank=False, validators=TagValidators
)
# TODO case convert and space removal on save
# TODO check on save
pinned = models.BooleanField(default=False, null=True)
class Meta:
unique_together = [["owner", "title"]]
@ -61,7 +60,27 @@ class Tag(List):
@staticmethod
def deep_cleanup_title(title):
"""Remove all non-word characters, only for public index purpose"""
return re.sub(r"\W+", " ", title).rstrip().lstrip("# ").lower() or "_"
return re.sub(r"\W+", " ", title).rstrip().lstrip("# ").lower()[:100] or "_"
def update(
self, title: str, visibility: int | None = None, pinned: bool | None = None
):
old_title = Tag.deep_cleanup_title(self.title)
new_title = Tag.deep_cleanup_title(title)
was_pinned = bool(self.pinned)
if visibility is not None:
self.visibility = 2 if visibility else 0
if pinned is not None:
self.pinned = pinned
self.title = title
self.save()
if was_pinned != self.pinned or (old_title != new_title and self.pinned):
from takahe.utils import Takahe
if was_pinned:
Takahe.unpin_hashtag_for_user(self.owner.pk, old_title)
if self.pinned:
Takahe.pin_hashtag_for_user(self.owner.pk, new_title)
class TagManager:

View file

@ -4,72 +4,57 @@
{% load humanize %}
{% load mastodon %}
{% load thumb %}
<div id="modal"
_="on closeModal add .closing then wait for animationend then remove me">
<div class="modal-underlay" _="on click trigger closeModal"></div>
<div class="modal-content">
<div class="add-to-list-modal__head">
<span class="add-to-list-modal__title">{% trans 'Tag' %} - {{ item.title }} - {% trans 'Edit' %}</span>
<span class="add-to-list-modal__close-button modal-close"
_="on click trigger closeModal">
<i class="fa-solid fa-xmark"></i>
</span>
</div>
<div class="add-to-list-modal__body">
<dialog open
class="tag-editor"
_="on close_dialog add .closing then wait for animationend then remove me">
<article>
<header>
<link to="#"
aria-label="Close"
class="close"
_="on click trigger close_dialog" />
<strong>{% trans 'Edit' %} {{ tag.title }}</strong>
</header>
<div>
<form action="{% url 'journal:user_tag_edit' %}?tag={{ tag.title }}"
method="post">
{% csrf_token %}
<input type="hidden" name="id" value="{{ tag.id }}">
<div class="mark-modal__tag">
<div class="tag-input">
<input name="title" type="text" value="{{ tag.title }}" required>
</div>
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>{% trans "Visibility" %}
<ul id="id_visibility">
<li>
<label for="id_visibility_0">
<input type="radio"
name="visibility"
value="0"
required=""
id="id_visibility_0"
{% if tag.visibility == 0 %}checked{% endif %}>
{% trans "Public" %}
</label>
</li>
<li>
<label for="id_visibility_2">
<input type="radio"
name="visibility"
value="2"
required=""
id="id_visibility_2"
{% if tag.visibility != 0 %}checked{% endif %}>
{% trans "Personal" %}
</label>
</li>
</ul>
</span>
</div>
<i>{% trans "Personal tags are not shown to others when they view your tag list. However, if you use this tag when marking an item publicly, it might still be visible to others." %}</i>
</div>
<div class="mark-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans "Save" %}">
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>
<label for="_delete">
<input type="checkbox" name="delete" value="1" id="_delete">
{% trans "Delete this tag" %}
</label>
</span>
</div>
</div>
<input name="title" type="text" value="{{ tag.title }}" required>
<fieldset>
<label for="_pinned">
<input role="switch"
type="checkbox"
name="pinned"
value="1"
{% if tag.pinned %}checked{% endif %}
id="_pinned">
{% trans "Pin" %}
</label>
</fieldset>
<fieldset>
<input type="radio"
name="visibility"
value="0"
required=""
id="id_visibility_0"
{% if tag.visibility == 0 %}checked{% endif %}>
<label for="id_visibility_0">{% trans "Public" %}</label>
<input type="radio"
name="visibility"
value="2"
required=""
id="id_visibility_2"
{% if tag.visibility != 0 %}checked{% endif %}>
<label for="id_visibility_2">{% trans "Personal" %}</label>
</fieldset>
<input type="submit" class="button float-right" value="{% trans "Save" %}">
<small>{% trans "Personal tags are not shown to others when they view your tag list, unless you pin them. However, if you use this tag when marking an item publicly, it might still be visible to others." %}</small>
<label for="_delete">
<input type="checkbox" name="delete" value="1" id="_delete">
{% trans "Delete this tag" %}
</label>
</form>
</div>
</div>
</div>
</article>
</dialog>

View file

@ -15,7 +15,7 @@
{% include "_header.html" %}
<main>
<div class="grid__main">
<h5 class="large-only">
<h5>
{% block head %}{{ identity.display_name }}{% endblock %}
</h5>
<div>

View file

@ -68,10 +68,11 @@ def user_tag_edit(request):
):
msg.error(request.user, _("Duplicated tag."))
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
tag.title = tag_title
tag.visibility = int(request.POST.get("visibility", 0))
tag.visibility = 0 if tag.visibility == 0 else 2
tag.save()
tag.update(
tag_title,
int(request.POST.get("visibility", 0)),
bool(request.POST.get("pinned", 0)),
)
msg.info(request.user, _("Tag updated."))
return redirect(
reverse(

View file

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-04 10:15-0400\n"
"POT-Creation-Date: 2024-06-04 16:48-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -667,7 +667,7 @@ msgstr "不再提示"
#: catalog/templates/_item_comments_by_episode.html:78
#: catalog/templates/_item_reviews.html:43
#: catalog/templates/podcast_episode_data.html:41
#: common/templates/_sidebar.html:203
#: common/templates/_sidebar.html:204
msgid "show more"
msgstr "显示更多"
@ -1018,7 +1018,7 @@ msgstr "否"
#: journal/templates/collection.html:132
#: journal/templates/collection_edit.html:9
#: journal/templates/collection_edit.html:25 journal/templates/review.html:80
#: journal/templates/tag_edit.html:12
#: journal/templates/tag_edit.html:16
msgid "Edit"
msgstr "编辑"
@ -1031,7 +1031,7 @@ msgstr "创建"
#: journal/templates/add_to_collection.html:35
#: journal/templates/collection_edit.html:38 journal/templates/comment.html:69
#: journal/templates/mark.html:147 journal/templates/review_edit.html:39
#: journal/templates/tag_edit.html:60 users/templates/users/account.html:43
#: journal/templates/tag_edit.html:51 users/templates/users/account.html:43
#: users/templates/users/account.html:104
#: users/templates/users/preferences.html:168
#: users/templates/users/preferences.html:193
@ -1112,8 +1112,8 @@ msgstr "热门标签"
#: catalog/templates/discover.html:185 catalog/templates/item_base.html:236
#: catalog/templates/item_mark_list.html:54
#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:98
#: common/templates/_sidebar.html:198
#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99
#: common/templates/_sidebar.html:199
#: common/templates/_sidebar_anonymous.html:43
#: common/templates/_sidebar_anonymous.html:58
#: journal/templates/collection_items.html:8 journal/templates/posts.html:45
@ -1544,31 +1544,31 @@ msgstr "设置用户名"
msgid "approving followers manually"
msgstr "已开启关注审核"
#: common/templates/_sidebar.html:83
#: common/templates/_sidebar.html:84
msgid "Current Targets"
msgstr "当前目标"
#: common/templates/_sidebar.html:96
#: common/templates/_sidebar.html:97
msgid "Set a collection as target, its progress will show up here."
msgstr "将自己或他人的收藏单设为目标,这里就会显示进度"
#: common/templates/_sidebar.html:109
#: common/templates/_sidebar.html:110
msgid "Recent podcast episodes"
msgstr "近期播客节目"
#: common/templates/_sidebar.html:144
#: common/templates/_sidebar.html:145
msgid "Currently reading"
msgstr "正在阅读"
#: common/templates/_sidebar.html:167
#: common/templates/_sidebar.html:168
msgid "Currently watching"
msgstr "正在追看"
#: common/templates/_sidebar.html:190
#: common/templates/_sidebar.html:191
msgid "Common Tags"
msgstr "常用标签"
#: common/templates/_sidebar.html:214
#: common/templates/_sidebar.html:215
msgid "Recent Posts"
msgstr "近期帖文"
@ -1722,8 +1722,8 @@ msgstr "发布到联邦宇宙"
#: journal/forms.py:25 journal/forms.py:45
#: journal/templates/collection_share.html:24
#: journal/templates/tag_edit.html:30 journal/templates/wrapped_share.html:36
#: users/templates/users/data.html:38 users/templates/users/data.html:130
#: journal/templates/wrapped_share.html:36 users/templates/users/data.html:38
#: users/templates/users/data.html:130
msgid "Visibility"
msgstr "可见性"
@ -1756,7 +1756,7 @@ msgstr "备注"
#: journal/models/common.py:28 journal/templates/collection_share.html:35
#: journal/templates/comment.html:35 journal/templates/mark.html:93
#: journal/templates/tag_edit.html:40 journal/templates/wrapped_share.html:43
#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43
#: users/templates/users/data.html:47 users/templates/users/data.html:139
#: users/templates/users/preferences.html:54
msgid "Public"
@ -2493,7 +2493,7 @@ msgstr "日历"
msgid "annual summary"
msgstr "年度小结"
#: journal/templates/profile.html:131 mastodon/api.py:680
#: journal/templates/profile.html:131 mastodon/api.py:670
msgid "collection"
msgstr "收藏单"
@ -2521,19 +2521,19 @@ msgstr "保存时将行首空格替换为全角"
msgid "change review date"
msgstr "指定评论日期"
#: journal/templates/tag_edit.html:12
msgid "Tag"
msgstr "标签"
#: journal/templates/tag_edit.html:32
msgid "Pin"
msgstr "置顶"
#: journal/templates/tag_edit.html:51
#: journal/templates/tag_edit.html:49
msgid "Personal"
msgstr "个人"
#: journal/templates/tag_edit.html:57
msgid "Personal tags are not shown to others when they view your tag list. However, if you use this tag when marking an item publicly, it might still be visible to others."
msgstr "个人标签仅限于在个人主页的标签列表里不向他人展示,如果公开标记一个条目时使用这个标签仍会被别人看到。"
#: journal/templates/tag_edit.html:52
msgid "Personal tags are not shown to others when they view your tag list, unless you pin them. However, if you use this tag when marking an item publicly, it might still be visible to others."
msgstr "个人标签不被包括在条目的公共索引中,但如果公开标记一个条目时使用这个标签仍会被别人看到。"
#: journal/templates/tag_edit.html:67
#: journal/templates/tag_edit.html:55
msgid "Delete this tag"
msgstr "删除这个标签"
@ -2685,7 +2685,7 @@ msgstr "标签已删除"
msgid "Duplicated tag."
msgstr "重复标签"
#: journal/views/tag.py:75
#: journal/views/tag.py:76
msgid "Tag updated."
msgstr "标签已更新"
@ -2698,11 +2698,11 @@ msgstr "总结已发布到时间轴"
msgid "regarding {item_title}, may contain spoiler or triggering content"
msgstr "关于 {item_title},可能包含剧透或敏感内容"
#: mastodon/api.py:685
#: mastodon/api.py:675
msgid "shared my collection"
msgstr "分享我的收藏单"
#: mastodon/api.py:688
#: mastodon/api.py:678
#, python-brace-format
msgid "shared {username}'s collection"
msgstr "分享 {username} 的收藏单"
@ -2964,31 +2964,31 @@ msgstr "提及"
msgid "follow"
msgstr "关注"
#: takahe/models.py:427
#: takahe/models.py:430
msgid "Display Name"
msgstr "昵称"
#: takahe/models.py:429
#: takahe/models.py:432
msgid "Bio"
msgstr "简介"
#: takahe/models.py:431
#: takahe/models.py:434
msgid "Manually approve new followers"
msgstr "手工审核关注者"
#: takahe/models.py:435
#: takahe/models.py:438
msgid "Include profile and posts in discovery"
msgstr "允许个人资料和帖文包含在发现中"
#: takahe/models.py:438
#: takahe/models.py:441
msgid "Include posts in search results"
msgstr "允许个人帖文包含在搜索结果中"
#: takahe/models.py:456
#: takahe/models.py:459
msgid "Profile picture"
msgstr "头像"
#: takahe/models.py:463
#: takahe/models.py:466
msgid "Header picture"
msgstr "背景图片"

View file

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-04 10:15-0400\n"
"POT-Creation-Date: 2024-06-04 16:48-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -667,7 +667,7 @@ msgstr "不再提示"
#: catalog/templates/_item_comments_by_episode.html:78
#: catalog/templates/_item_reviews.html:43
#: catalog/templates/podcast_episode_data.html:41
#: common/templates/_sidebar.html:203
#: common/templates/_sidebar.html:204
msgid "show more"
msgstr "顯示更多"
@ -1018,7 +1018,7 @@ msgstr "否"
#: journal/templates/collection.html:132
#: journal/templates/collection_edit.html:9
#: journal/templates/collection_edit.html:25 journal/templates/review.html:80
#: journal/templates/tag_edit.html:12
#: journal/templates/tag_edit.html:16
msgid "Edit"
msgstr "編輯"
@ -1031,7 +1031,7 @@ msgstr "創建"
#: journal/templates/add_to_collection.html:35
#: journal/templates/collection_edit.html:38 journal/templates/comment.html:69
#: journal/templates/mark.html:147 journal/templates/review_edit.html:39
#: journal/templates/tag_edit.html:60 users/templates/users/account.html:43
#: journal/templates/tag_edit.html:51 users/templates/users/account.html:43
#: users/templates/users/account.html:104
#: users/templates/users/preferences.html:168
#: users/templates/users/preferences.html:193
@ -1112,8 +1112,8 @@ msgstr "熱門標籤"
#: catalog/templates/discover.html:185 catalog/templates/item_base.html:236
#: catalog/templates/item_mark_list.html:54
#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:98
#: common/templates/_sidebar.html:198
#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99
#: common/templates/_sidebar.html:199
#: common/templates/_sidebar_anonymous.html:43
#: common/templates/_sidebar_anonymous.html:58
#: journal/templates/collection_items.html:8 journal/templates/posts.html:45
@ -1544,31 +1544,31 @@ msgstr "設置用戶名"
msgid "approving followers manually"
msgstr "已開啓關注審覈"
#: common/templates/_sidebar.html:83
#: common/templates/_sidebar.html:84
msgid "Current Targets"
msgstr "當前目標"
#: common/templates/_sidebar.html:96
#: common/templates/_sidebar.html:97
msgid "Set a collection as target, its progress will show up here."
msgstr "將自己或他人的收藏單設爲目標,這裏就會顯示進度"
#: common/templates/_sidebar.html:109
#: common/templates/_sidebar.html:110
msgid "Recent podcast episodes"
msgstr "近期播客節目"
#: common/templates/_sidebar.html:144
#: common/templates/_sidebar.html:145
msgid "Currently reading"
msgstr "正在閱讀"
#: common/templates/_sidebar.html:167
#: common/templates/_sidebar.html:168
msgid "Currently watching"
msgstr "正在追看"
#: common/templates/_sidebar.html:190
#: common/templates/_sidebar.html:191
msgid "Common Tags"
msgstr "常用標籤"
#: common/templates/_sidebar.html:214
#: common/templates/_sidebar.html:215
msgid "Recent Posts"
msgstr "近期帖文"
@ -1722,8 +1722,8 @@ msgstr "發佈到聯邦宇宙"
#: journal/forms.py:25 journal/forms.py:45
#: journal/templates/collection_share.html:24
#: journal/templates/tag_edit.html:30 journal/templates/wrapped_share.html:36
#: users/templates/users/data.html:38 users/templates/users/data.html:130
#: journal/templates/wrapped_share.html:36 users/templates/users/data.html:38
#: users/templates/users/data.html:130
msgid "Visibility"
msgstr "可見性"
@ -1756,7 +1756,7 @@ msgstr "備註"
#: journal/models/common.py:28 journal/templates/collection_share.html:35
#: journal/templates/comment.html:35 journal/templates/mark.html:93
#: journal/templates/tag_edit.html:40 journal/templates/wrapped_share.html:43
#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43
#: users/templates/users/data.html:47 users/templates/users/data.html:139
#: users/templates/users/preferences.html:54
msgid "Public"
@ -2493,7 +2493,7 @@ msgstr "日曆"
msgid "annual summary"
msgstr "年度小結"
#: journal/templates/profile.html:131 mastodon/api.py:680
#: journal/templates/profile.html:131 mastodon/api.py:670
msgid "collection"
msgstr "收藏單"
@ -2521,19 +2521,19 @@ msgstr "保存時將行首空格替換爲全角"
msgid "change review date"
msgstr "指定評論日期"
#: journal/templates/tag_edit.html:12
msgid "Tag"
msgstr "標籤"
#: journal/templates/tag_edit.html:32
msgid "Pin"
msgstr "置頂"
#: journal/templates/tag_edit.html:51
#: journal/templates/tag_edit.html:49
msgid "Personal"
msgstr "個人"
#: journal/templates/tag_edit.html:57
msgid "Personal tags are not shown to others when they view your tag list. However, if you use this tag when marking an item publicly, it might still be visible to others."
msgstr "個人標籤僅限於在個人主頁的標籤列表裏不向他人展示,如果公開標記一個條目時使用這個標籤仍會被別人看到。"
#: journal/templates/tag_edit.html:52
msgid "Personal tags are not shown to others when they view your tag list, unless you pin them. However, if you use this tag when marking an item publicly, it might still be visible to others."
msgstr "個人標籤不被包括在條目的公共索引中,但如果公開標記一個條目時使用這個標籤仍會被別人看到。"
#: journal/templates/tag_edit.html:67
#: journal/templates/tag_edit.html:55
msgid "Delete this tag"
msgstr "刪除這個標籤"
@ -2685,7 +2685,7 @@ msgstr "標籤已刪除"
msgid "Duplicated tag."
msgstr "重複標籤"
#: journal/views/tag.py:75
#: journal/views/tag.py:76
msgid "Tag updated."
msgstr "標籤已更新"
@ -2698,11 +2698,11 @@ msgstr "總結已發佈到時間軸"
msgid "regarding {item_title}, may contain spoiler or triggering content"
msgstr "關於 {item_title},可能包含劇透或敏感內容"
#: mastodon/api.py:685
#: mastodon/api.py:675
msgid "shared my collection"
msgstr "分享我的收藏單"
#: mastodon/api.py:688
#: mastodon/api.py:678
#, python-brace-format
msgid "shared {username}'s collection"
msgstr "分享 {username} 的收藏單"
@ -2964,31 +2964,31 @@ msgstr "提及"
msgid "follow"
msgstr "關注"
#: takahe/models.py:427
#: takahe/models.py:430
msgid "Display Name"
msgstr "暱稱"
#: takahe/models.py:429
#: takahe/models.py:432
msgid "Bio"
msgstr "簡介"
#: takahe/models.py:431
#: takahe/models.py:434
msgid "Manually approve new followers"
msgstr "手工審覈關注者"
#: takahe/models.py:435
#: takahe/models.py:438
msgid "Include profile and posts in discovery"
msgstr "允許個人資料和帖文包含在發現中"
#: takahe/models.py:438
#: takahe/models.py:441
msgid "Include posts in search results"
msgstr "允許個人帖文包含在搜索結果中"
#: takahe/models.py:456
#: takahe/models.py:459
msgid "Profile picture"
msgstr "頭像"
#: takahe/models.py:463
#: takahe/models.py:466
msgid "Header picture"
msgstr "背景圖片"

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.13 on 2024-06-01 05:38
# Generated by Django 4.2.13 on 2024-06-04 19:33
import functools
@ -102,6 +102,11 @@ class Migration(migrations.Migration):
("public", models.BooleanField(null=True)),
("state", models.CharField(default="outdated", max_length=100)),
("state_changed", models.DateTimeField(auto_now_add=True)),
("state_next_attempt", models.DateTimeField(blank=True, null=True)),
(
"state_locked_until",
models.DateTimeField(blank=True, db_index=True, null=True),
),
("stats", models.JSONField(blank=True, null=True)),
("stats_updated", models.DateTimeField(blank=True, null=True)),
("aliases", models.JSONField(blank=True, null=True)),
@ -162,8 +167,7 @@ class Migration(migrations.Migration):
(
"indexable",
models.BooleanField(
default=True,
verbose_name="Include posts in search results",
default=True, verbose_name="Include posts in search results"
),
),
(
@ -589,6 +593,40 @@ class Migration(migrations.Migration):
blank=True, related_name="identities", to="takahe.user"
),
),
migrations.CreateModel(
name="HashtagFeature",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"hashtag",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="featurers",
to="takahe.hashtag",
),
),
(
"identity",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hashtag_features",
to="takahe.identity",
),
),
],
options={
"db_table": "users_hashtagfeature",
},
),
migrations.CreateModel(
name="FanOut",
fields=[

View file

@ -5,7 +5,7 @@ import re
import secrets
import ssl
import time
from datetime import date
from datetime import date, timedelta
from functools import cached_property, partial
from typing import TYPE_CHECKING, Literal, Optional
from urllib.parse import urlparse
@ -385,7 +385,10 @@ class Identity(models.Model):
Represents both local and remote Fediverse identities (actors)
"""
domain_id: str
if TYPE_CHECKING:
domain_id: str
inbound_follows: "models.QuerySet[Follow]"
hashtag_features: "models.QuerySet[HashtagFeature]"
class Restriction(models.IntegerChoices):
none = 0
@ -729,6 +732,34 @@ class Identity(models.Model):
self.shared_inbox_uri = f"https://{self.domain.uri_domain}/inbox/"
self.save()
def get_remote_targets(self):
"""
Returns an iterable with Identities of followers that have unique
shared_inbox among each other to be used as target.
"""
if not self.local:
return []
remote_follower_ids = Follow.objects.filter(
target=self,
target__local=False,
state__in=["unrequested", "pending_approval", "accepting", "accepted"],
).values_list("source", flat=True)
deduped_targets = set()
shared_inboxes = set()
for target in Identity.objects.filter(pk__in=remote_follower_ids):
if not target.shared_inbox_uri:
deduped_targets.add(target)
elif target.shared_inbox_uri not in shared_inboxes:
shared_inboxes.add(target.shared_inbox_uri)
deduped_targets.add(target)
return deduped_targets
def fanout(self, type: str, **kwargs):
for target in self.get_remote_targets():
FanOut.objects.create(
identity=target, subject_identity=self, type=type, **kwargs
)
class Follow(models.Model):
"""
@ -1569,6 +1600,8 @@ class Hashtag(models.Model):
# state = StateField(HashtagStates)
state = models.CharField(max_length=100, default="outdated")
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)
# Metrics for this Hashtag
stats = models.JSONField(null=True, blank=True)
@ -1639,6 +1672,53 @@ class Hashtag(models.Model):
results[date(year, month, day)] = val
return dict(sorted(results.items(), reverse=True)[:num])
@property
def needs_update(self):
if self.stats_updated is None:
return True
return timezone.now() - self.stats_updated > timedelta(hours=1)
@classmethod
def ensure_hashtag(cls, name, update=None):
"""
Properly strips/trims/lowercases the hashtag name, and makes sure a Hashtag
object exists in the database, and returns it.
"""
name = name.strip().lstrip("#").lower()[: Hashtag.MAXIMUM_LENGTH]
hashtag, created = cls.objects.get_or_create(hashtag=name)
if created or update or hashtag.needs_update:
hashtag.state = "outdated"
hashtag.state_changed = timezone.now()
hashtag.state_next_attempt = None
hashtag.state_locked_until = None
hashtag.save(
update_fields=[
"state",
"state_changed",
"state_next_attempt",
"state_locked_until",
]
)
return hashtag
class HashtagFeature(models.Model):
identity = models.ForeignKey(
"takahe.Identity",
on_delete=models.CASCADE,
related_name="hashtag_features",
)
hashtag = models.ForeignKey(
"takahe.Hashtag",
on_delete=models.CASCADE,
related_name="featurers",
)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "users_hashtagfeature"
class PostInteraction(models.Model):
"""

View file

@ -990,3 +990,21 @@ class Takahe:
else:
qs = qs.filter(visibility__in=[0, 1, 4])
return qs.prefetch_related("attachments", "author")
@staticmethod
def pin_hashtag_for_user(identity_pk: int, hashtag: str):
tag = Hashtag.ensure_hashtag(hashtag)
identity = Identity.objects.get(pk=identity_pk)
feature, created = identity.hashtag_features.get_or_create(hashtag=tag)
if created:
identity.fanout("tag_featured", subject_hashtag=tag)
@staticmethod
def unpin_hashtag_for_user(identity_pk: int, hashtag: str):
identity = Identity.objects.get(pk=identity_pk)
featured = HashtagFeature.objects.filter(
identity=identity, hashtag_id=hashtag
).first()
if featured:
identity.fanout("tag_unfeatured", subject_hashtag_id=hashtag)
featured.delete()