Christof Dorner 2023-06-14 20:07:11 +02:00 zatwierdzone przez GitHub
commit f14940cbd7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
12 zmienionych plików z 147 dodań i 8 usunięć

Wyświetl plik

@ -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=""),
),
]

Wyświetl plik

@ -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,
} }

Wyświetl plik

@ -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,
} }

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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 = ""

Wyświetl plik

@ -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

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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",
],
} }

Wyświetl plik

@ -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,