kopia lustrzana https://github.com/jointakahe/takahe
Merge 9204ae5a93
into bb8f589da7
commit
f14940cbd7
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-05-15 09:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activities", "0016_index_together_migration"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="post",
|
||||||
|
name="language",
|
||||||
|
field=models.CharField(default=""),
|
||||||
|
),
|
||||||
|
]
|
|
@ -30,6 +30,7 @@ from core.html import ContentRenderer, FediverseHtmlParser
|
||||||
from core.ld import (
|
from core.ld import (
|
||||||
canonicalise,
|
canonicalise,
|
||||||
format_ld_date,
|
format_ld_date,
|
||||||
|
get_language,
|
||||||
get_list,
|
get_list,
|
||||||
get_value_or_map,
|
get_value_or_map,
|
||||||
parse_ld_date,
|
parse_ld_date,
|
||||||
|
@ -251,6 +252,9 @@ class Post(StatorModel):
|
||||||
# The main (HTML) content
|
# The main (HTML) content
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
|
|
||||||
|
# The language of the content
|
||||||
|
language = models.CharField(default="")
|
||||||
|
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=Types.choices,
|
choices=Types.choices,
|
||||||
|
@ -473,6 +477,7 @@ class Post(StatorModel):
|
||||||
reply_to: Optional["Post"] = None,
|
reply_to: Optional["Post"] = None,
|
||||||
attachments: list | None = None,
|
attachments: list | None = None,
|
||||||
question: dict | None = None,
|
question: dict | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> "Post":
|
) -> "Post":
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Find mentions in this post
|
# Find mentions in this post
|
||||||
|
@ -491,6 +496,9 @@ class Post(StatorModel):
|
||||||
sorted([tag[: Hashtag.MAXIMUM_LENGTH] for tag in parser.hashtags])
|
sorted([tag[: Hashtag.MAXIMUM_LENGTH] for tag in parser.hashtags])
|
||||||
or None
|
or None
|
||||||
)
|
)
|
||||||
|
if language is None or language == "":
|
||||||
|
language = author.config_identity.preferred_posting_language
|
||||||
|
|
||||||
# Make the Post object
|
# Make the Post object
|
||||||
post = cls.objects.create(
|
post = cls.objects.create(
|
||||||
author=author,
|
author=author,
|
||||||
|
@ -501,6 +509,7 @@ class Post(StatorModel):
|
||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
hashtags=hashtags,
|
hashtags=hashtags,
|
||||||
in_reply_to=reply_to.object_uri if reply_to else None,
|
in_reply_to=reply_to.object_uri if reply_to else None,
|
||||||
|
language=language,
|
||||||
)
|
)
|
||||||
post.object_uri = post.urls.object_uri
|
post.object_uri = post.urls.object_uri
|
||||||
post.url = post.absolute_object_uri()
|
post.url = post.absolute_object_uri()
|
||||||
|
@ -525,6 +534,7 @@ class Post(StatorModel):
|
||||||
visibility: int = Visibilities.public,
|
visibility: int = Visibilities.public,
|
||||||
attachments: list | None = None,
|
attachments: list | None = None,
|
||||||
attachment_attributes: list | None = None,
|
attachment_attributes: list | None = None,
|
||||||
|
language: str | None = None,
|
||||||
):
|
):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Strip all HTML and apply linebreaks filter
|
# Strip all HTML and apply linebreaks filter
|
||||||
|
@ -537,6 +547,9 @@ class Post(StatorModel):
|
||||||
self.summary = summary or None
|
self.summary = summary or None
|
||||||
self.sensitive = bool(summary) if sensitive is None else sensitive
|
self.sensitive = bool(summary) if sensitive is None else sensitive
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
|
if language is None or language == "":
|
||||||
|
language = self.author.config_identity.preferred_posting_language
|
||||||
|
self.language = language
|
||||||
self.edited = timezone.now()
|
self.edited = timezone.now()
|
||||||
self.mentions.set(self.mentions_from_content(content, self.author))
|
self.mentions.set(self.mentions_from_content(content, self.author))
|
||||||
self.emojis.set(Emoji.emojis_from_content(content, None))
|
self.emojis.set(Emoji.emojis_from_content(content, None))
|
||||||
|
@ -648,6 +661,10 @@ class Post(StatorModel):
|
||||||
"tag": [],
|
"tag": [],
|
||||||
"attachment": [],
|
"attachment": [],
|
||||||
}
|
}
|
||||||
|
if self.language != "":
|
||||||
|
value["contentMap"] = {
|
||||||
|
self.language: value["content"],
|
||||||
|
}
|
||||||
if self.type == Post.Types.question and self.type_data:
|
if self.type == Post.Types.question and self.type_data:
|
||||||
value[self.type_data.mode] = [
|
value[self.type_data.mode] = [
|
||||||
{
|
{
|
||||||
|
@ -871,6 +888,7 @@ class Post(StatorModel):
|
||||||
post.published = parse_ld_date(data.get("published"))
|
post.published = parse_ld_date(data.get("published"))
|
||||||
post.edited = parse_ld_date(data.get("updated"))
|
post.edited = parse_ld_date(data.get("updated"))
|
||||||
post.in_reply_to = data.get("inReplyTo")
|
post.in_reply_to = data.get("inReplyTo")
|
||||||
|
post.language = get_language(data) or ""
|
||||||
# Mentions and hashtags
|
# Mentions and hashtags
|
||||||
post.hashtags = []
|
post.hashtags = []
|
||||||
for tag in get_list(data, "tag"):
|
for tag in get_list(data, "tag"):
|
||||||
|
@ -1105,12 +1123,16 @@ class Post(StatorModel):
|
||||||
self.Visibilities.mentioned: "direct",
|
self.Visibilities.mentioned: "direct",
|
||||||
self.Visibilities.local_only: "public",
|
self.Visibilities.local_only: "public",
|
||||||
}
|
}
|
||||||
|
language = self.language
|
||||||
|
if self.language == "":
|
||||||
|
language = None
|
||||||
value = {
|
value = {
|
||||||
"id": self.pk,
|
"id": self.pk,
|
||||||
"uri": self.object_uri,
|
"uri": self.object_uri,
|
||||||
"created_at": format_ld_date(self.published),
|
"created_at": format_ld_date(self.published),
|
||||||
"account": self.author.to_mastodon_json(include_counts=False),
|
"account": self.author.to_mastodon_json(include_counts=False),
|
||||||
"content": self.safe_content_remote(),
|
"content": self.safe_content_remote(),
|
||||||
|
"language": language,
|
||||||
"visibility": visibility_mapping[self.visibility],
|
"visibility": visibility_mapping[self.visibility],
|
||||||
"sensitive": self.sensitive,
|
"sensitive": self.sensitive,
|
||||||
"spoiler_text": self.summary or "",
|
"spoiler_text": self.summary or "",
|
||||||
|
@ -1151,7 +1173,6 @@ class Post(StatorModel):
|
||||||
if isinstance(self.type_data, QuestionData)
|
if isinstance(self.type_data, QuestionData)
|
||||||
else None,
|
else None,
|
||||||
"card": None,
|
"card": None,
|
||||||
"language": None,
|
|
||||||
"text": self.safe_content_remote(),
|
"text": self.safe_content_remote(),
|
||||||
"edited_at": format_ld_date(self.edited) if self.edited else None,
|
"edited_at": format_ld_date(self.edited) if self.edited else None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,7 @@ class Status(Schema):
|
||||||
reblog: Optional["Status"] = Field(...)
|
reblog: Optional["Status"] = Field(...)
|
||||||
poll: Poll | None = Field(...)
|
poll: Poll | None = Field(...)
|
||||||
card: None = Field(...)
|
card: None = Field(...)
|
||||||
language: None = Field(...)
|
language: str | None = Field(...)
|
||||||
text: str | None = Field(...)
|
text: str | None = Field(...)
|
||||||
edited_at: str | None
|
edited_at: str | None
|
||||||
favourited: bool = False
|
favourited: bool = False
|
||||||
|
@ -422,13 +422,19 @@ class Preferences(Schema):
|
||||||
activities_models.Post.Visibilities.mentioned: "direct",
|
activities_models.Post.Visibilities.mentioned: "direct",
|
||||||
activities_models.Post.Visibilities.local_only: "public",
|
activities_models.Post.Visibilities.local_only: "public",
|
||||||
}
|
}
|
||||||
|
preferred_posting_language = None
|
||||||
|
if identity.config_identity.preferred_posting_language != "":
|
||||||
|
preferred_posting_language = (
|
||||||
|
identity.config_identity.preferred_posting_language
|
||||||
|
)
|
||||||
|
|
||||||
return cls.parse_obj(
|
return cls.parse_obj(
|
||||||
{
|
{
|
||||||
"posting:default:visibility": visibility_mapping[
|
"posting:default:visibility": visibility_mapping[
|
||||||
identity.config_identity.default_post_visibility
|
identity.config_identity.default_post_visibility
|
||||||
],
|
],
|
||||||
"posting:default:sensitive": False,
|
"posting:default:sensitive": False,
|
||||||
"posting:default:language": None,
|
"posting:default:language": preferred_posting_language,
|
||||||
"reading:expand:media": "default",
|
"reading:expand:media": "default",
|
||||||
"reading:expand:spoilers": identity.config_identity.expand_content_warnings,
|
"reading:expand:spoilers": identity.config_identity.expand_content_warnings,
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ def post_status(request, details: PostStatusSchema) -> schemas.Status:
|
||||||
reply_to=reply_post,
|
reply_to=reply_post,
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
question=details.poll.dict() if details.poll else None,
|
question=details.poll.dict() if details.poll else None,
|
||||||
|
language=details.language,
|
||||||
)
|
)
|
||||||
# Add their own timeline event for immediate visibility
|
# Add their own timeline event for immediate visibility
|
||||||
TimelineEvent.add_post(request.identity, post)
|
TimelineEvent.add_post(request.identity, post)
|
||||||
|
@ -141,6 +142,7 @@ def edit_status(request, id: str, details: EditStatusSchema) -> schemas.Status:
|
||||||
sensitive=details.sensitive,
|
sensitive=details.sensitive,
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
attachment_attributes=details.media_attributes,
|
attachment_attributes=details.media_attributes,
|
||||||
|
language=details.language,
|
||||||
)
|
)
|
||||||
return schemas.Status.from_post(post)
|
return schemas.Status.from_post(post)
|
||||||
|
|
||||||
|
|
22
core/ld.py
22
core/ld.py
|
@ -1,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import urllib.parse as urllib_parse
|
import urllib.parse as urllib_parse
|
||||||
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
@ -692,3 +693,24 @@ def media_type_from_filename(filename):
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
else:
|
else:
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
|
def get_language(data) -> str | None:
|
||||||
|
"""Detects and returns a document's language"""
|
||||||
|
map_ = None
|
||||||
|
if "contentMap" in data:
|
||||||
|
map_ = data["contentMap"]
|
||||||
|
elif "nameMap" in data:
|
||||||
|
map_ = data["nameMap"]
|
||||||
|
elif "summaryMap" in data:
|
||||||
|
map_ = data["summaryMap"]
|
||||||
|
|
||||||
|
if not map_:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lang = list(map_.keys())[0]
|
||||||
|
if not lang or lang == "und":
|
||||||
|
return None
|
||||||
|
|
||||||
|
lang = re.split("-|_", lang)[0]
|
||||||
|
return lang.lower()
|
||||||
|
|
|
@ -286,6 +286,7 @@ class Config(models.Model):
|
||||||
visible_reaction_counts: bool = True
|
visible_reaction_counts: bool = True
|
||||||
expand_content_warnings: bool = False
|
expand_content_warnings: bool = False
|
||||||
boosts_on_profile: bool = True
|
boosts_on_profile: bool = True
|
||||||
|
preferred_posting_language: str = ""
|
||||||
|
|
||||||
class DomainOptions(pydantic.BaseModel):
|
class DomainOptions(pydantic.BaseModel):
|
||||||
site_name: str = ""
|
site_name: str = ""
|
||||||
|
|
|
@ -16,6 +16,7 @@ httpx~=0.23
|
||||||
markdown_it_py~=2.1.0
|
markdown_it_py~=2.1.0
|
||||||
pillow~=9.3.0
|
pillow~=9.3.0
|
||||||
psycopg~=3.1.8
|
psycopg~=3.1.8
|
||||||
|
pycountry~=22.3.5
|
||||||
pydantic~=1.10.2
|
pydantic~=1.10.2
|
||||||
pyld~=2.0.3
|
pyld~=2.0.3
|
||||||
pylibmc~=1.6.3
|
pylibmc~=1.6.3
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="content {% if post.summary %}hidden {% endif %}">
|
<div class="content {% if post.summary %}hidden {% endif %}"{% if post.language %} lang="{{ post.language }}"{% endif %}>
|
||||||
{{ post.safe_content_local }}
|
{{ post.safe_content_local }}
|
||||||
|
|
||||||
{% if post.attachments.exists %}
|
{% if post.attachments.exists %}
|
||||||
|
|
|
@ -259,6 +259,7 @@ def test_content_map(remote_identity):
|
||||||
create=True,
|
create=True,
|
||||||
)
|
)
|
||||||
assert post.content == "Hi World"
|
assert post.content == "Hi World"
|
||||||
|
assert post.language == ""
|
||||||
|
|
||||||
post2 = Post.by_ap(
|
post2 = Post.by_ap(
|
||||||
data={
|
data={
|
||||||
|
@ -271,6 +272,7 @@ def test_content_map(remote_identity):
|
||||||
create=True,
|
create=True,
|
||||||
)
|
)
|
||||||
assert post2.content == "Hey World"
|
assert post2.content == "Hey World"
|
||||||
|
assert post2.language == ""
|
||||||
|
|
||||||
post3 = Post.by_ap(
|
post3 = Post.by_ap(
|
||||||
data={
|
data={
|
||||||
|
@ -283,6 +285,7 @@ def test_content_map(remote_identity):
|
||||||
create=True,
|
create=True,
|
||||||
)
|
)
|
||||||
assert post3.content == "Hello World"
|
assert post3.content == "Hello World"
|
||||||
|
assert post3.language == "en"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
|
|
||||||
from dateutil.tz import tzutc
|
from dateutil.tz import tzutc
|
||||||
|
|
||||||
from core.ld import parse_ld_date
|
from core.ld import get_language, parse_ld_date
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ld_date():
|
def test_parse_ld_date():
|
||||||
|
@ -41,3 +41,41 @@ def test_parse_ld_date():
|
||||||
tzinfo=tzutc(),
|
tzinfo=tzutc(),
|
||||||
)
|
)
|
||||||
assert difference.total_seconds() == 0
|
assert difference.total_seconds() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_language():
|
||||||
|
assert (
|
||||||
|
get_language(
|
||||||
|
{
|
||||||
|
"contentMap": {
|
||||||
|
"en": "<p>Hello</p>",
|
||||||
|
"es": "<p>hola</p>",
|
||||||
|
},
|
||||||
|
"nameMap": {"de": "Hallo"},
|
||||||
|
"summaryMap": {"fr": "Bonjour"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "en"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
get_language(
|
||||||
|
{
|
||||||
|
"nameMap": {"de": "Hallo"},
|
||||||
|
"summaryMap": {"fr": "Bonjour"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "de"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
get_language(
|
||||||
|
{
|
||||||
|
"summaryMap": {"fr": "Bonjour"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "fr"
|
||||||
|
)
|
||||||
|
assert get_language({"contentMap": {"en-gb": "<p>Hello</p>"}}) == "en"
|
||||||
|
assert get_language({"contentMap": {"en_GB": "<p>Hello</p>"}}) == "en"
|
||||||
|
assert get_language({"contentMap": {"EN": "<p>Hello</p>"}}) == "en"
|
||||||
|
assert get_language({"contentMap": {"und": "<p>Hello</p>"}}) is None
|
||||||
|
assert get_language({}) is None
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pycountry
|
||||||
|
|
||||||
from activities.models.post import Post
|
from activities.models.post import Post
|
||||||
from users.views.settings.settings_page import SettingsPage
|
from users.views.settings.settings_page import SettingsPage
|
||||||
|
|
||||||
|
@ -15,8 +17,29 @@ class PostingPage(SettingsPage):
|
||||||
"title": "Expand content warnings",
|
"title": "Expand content warnings",
|
||||||
"help_text": "If content warnings should be expanded by default (not honoured by all clients)",
|
"help_text": "If content warnings should be expanded by default (not honoured by all clients)",
|
||||||
},
|
},
|
||||||
|
"preferred_posting_language": {
|
||||||
|
"title": "Default posting language",
|
||||||
|
"help_text": "",
|
||||||
|
"choices": sorted(
|
||||||
|
(
|
||||||
|
[
|
||||||
|
("", ""),
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
(lang.alpha_2, lang.name)
|
||||||
|
for lang in pycountry.languages
|
||||||
|
if hasattr(lang, "alpha_2")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
key=lambda lang: lang[1],
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = {
|
layout = {
|
||||||
"Posting": ["default_post_visibility", "expand_content_warnings"],
|
"Posting": [
|
||||||
|
"default_post_visibility",
|
||||||
|
"expand_content_warnings",
|
||||||
|
"preferred_posting_language",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class SettingsPage(FormView):
|
||||||
options_class = Config.IdentityOptions
|
options_class = Config.IdentityOptions
|
||||||
template_name = "settings/settings.html"
|
template_name = "settings/settings.html"
|
||||||
section: ClassVar[str]
|
section: ClassVar[str]
|
||||||
options: dict[str, dict[str, str | int]]
|
options: dict[str, dict[str, str | int | list[tuple[int | str, str]]]]
|
||||||
layout: dict[str, list[str]]
|
layout: dict[str, list[str]]
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
|
@ -42,7 +42,11 @@ class SettingsPage(FormView):
|
||||||
elif config_field.type_ is UploadedImage:
|
elif config_field.type_ is UploadedImage:
|
||||||
form_field = forms.ImageField
|
form_field = forms.ImageField
|
||||||
elif config_field.type_ is str:
|
elif config_field.type_ is str:
|
||||||
if details.get("display") == "textarea":
|
choices = details.get("choices")
|
||||||
|
if choices:
|
||||||
|
field_kwargs["widget"] = forms.Select(choices=choices)
|
||||||
|
form_field = forms.CharField
|
||||||
|
elif details.get("display") == "textarea":
|
||||||
form_field = partial(
|
form_field = partial(
|
||||||
forms.CharField,
|
forms.CharField,
|
||||||
widget=forms.Textarea,
|
widget=forms.Textarea,
|
||||||
|
|
Ładowanie…
Reference in New Issue