basic i18n support
This commit is contained in:
parent
3e301ad453
commit
fca9b6155d
26 changed files with 2709 additions and 90 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -42,3 +42,7 @@ log
|
||||||
|
|
||||||
# test coverage
|
# test coverage
|
||||||
/.coverage
|
/.coverage
|
||||||
|
|
||||||
|
|
||||||
|
# translations
|
||||||
|
*.mo
|
||||||
|
|
|
@ -36,13 +36,14 @@ RUN --mount=type=cache,sharing=locked,target=/var/cache/apt-run apt-get update \
|
||||||
RUN busybox --install
|
RUN busybox --install
|
||||||
|
|
||||||
# postgresql and redis cli are not required, but install for development convenience
|
# postgresql and redis cli are not required, but install for development convenience
|
||||||
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt-run apt-get install -y --no-install-recommends postgresql-client redis-tools
|
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt-run apt-get install -y --no-install-recommends postgresql-client redis-tools gettext
|
||||||
RUN useradd -U app
|
RUN useradd -U app
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
RUN rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=build /neodb /neodb
|
COPY --from=build /neodb /neodb
|
||||||
WORKDIR /neodb
|
WORKDIR /neodb
|
||||||
COPY --from=build /neodb-venv /neodb-venv
|
COPY --from=build /neodb-venv /neodb-venv
|
||||||
|
RUN /neodb-venv/bin/django-admin compilemessages
|
||||||
RUN NEODB_SECRET_KEY="t" NEODB_SITE_DOMAIN="x.y" NEODB_SITE_NAME="z" /neodb-venv/bin/python3 manage.py compilescss
|
RUN NEODB_SECRET_KEY="t" NEODB_SITE_DOMAIN="x.y" NEODB_SITE_NAME="z" /neodb-venv/bin/python3 manage.py compilescss
|
||||||
RUN NEODB_SECRET_KEY="t" NEODB_SITE_DOMAIN="x.y" NEODB_SITE_NAME="z" /neodb-venv/bin/python3 manage.py collectstatic --noinput
|
RUN NEODB_SECRET_KEY="t" NEODB_SITE_DOMAIN="x.y" NEODB_SITE_NAME="z" /neodb-venv/bin/python3 manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,8 @@ env = environ.FileAwareEnv(
|
||||||
),
|
),
|
||||||
# Links in site footer
|
# Links in site footer
|
||||||
NEODB_SITE_LINKS=(dict, {}),
|
NEODB_SITE_LINKS=(dict, {}),
|
||||||
|
# Default language
|
||||||
|
NEODB_LANGUAGE=(str, "zh-hans"),
|
||||||
# Invite only mode
|
# Invite only mode
|
||||||
# when True: user will not be able to register unless with invite token
|
# when True: user will not be able to register unless with invite token
|
||||||
# (generated by `neodb-manage invite --create`)
|
# (generated by `neodb-manage invite --create`)
|
||||||
|
@ -323,7 +325,8 @@ MIDDLEWARE = [
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"hijack.middleware.HijackUserMiddleware",
|
"hijack.middleware.HijackUserMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
# "django.middleware.locale.LocaleMiddleware",
|
||||||
|
"users.middlewares.LanguageMiddleware",
|
||||||
"tz_detect.middleware.TimezoneMiddleware",
|
"tz_detect.middleware.TimezoneMiddleware",
|
||||||
"auditlog.middleware.AuditlogMiddleware",
|
"auditlog.middleware.AuditlogMiddleware",
|
||||||
# "maintenance_mode.middleware.MaintenanceModeMiddleware", # this should be last if enabled
|
# "maintenance_mode.middleware.MaintenanceModeMiddleware", # this should be last if enabled
|
||||||
|
@ -384,15 +387,12 @@ if SLACK_TOKEN:
|
||||||
|
|
||||||
MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.models.render_md"
|
MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.models.render_md"
|
||||||
|
|
||||||
# Internationalization
|
LANGUAGE_CODE = env("NEODB_LANGUAGE", default="zh-hans") # type: ignore
|
||||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = "zh-hans"
|
|
||||||
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
||||||
# LANGUAGES = (
|
LANGUAGES = (
|
||||||
# ("en", _("English")),
|
# ("en", _("English")),
|
||||||
# ("zh-hans", _("Simplified Chinese")),
|
("zh-hans", _("Simplified Chinese")),
|
||||||
# )
|
)
|
||||||
|
|
||||||
TIME_ZONE = env("NEODB_TIMEZONE", default="Asia/Shanghai") # type: ignore
|
TIME_ZONE = env("NEODB_TIMEZONE", default="Asia/Shanghai") # type: ignore
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ItemType(models.TextChoices):
|
||||||
PodcastEpisode = "podcastepisode", _("Podcast Episode")
|
PodcastEpisode = "podcastepisode", _("Podcast Episode")
|
||||||
Performance = "performance", _("Performance")
|
Performance = "performance", _("Performance")
|
||||||
PerformanceProduction = "production", _("Production")
|
PerformanceProduction = "production", _("Production")
|
||||||
FanFic = "fanfic", _("Fanfix")
|
FanFic = "fanfic", _("Fanfic")
|
||||||
Exhibition = "exhibition", _("Exhibition")
|
Exhibition = "exhibition", _("Exhibition")
|
||||||
Collection = "collection", _("Collection")
|
Collection = "collection", _("Collection")
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import date
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.translation import pgettext_lazy
|
||||||
|
|
||||||
from catalog.common import (
|
from catalog.common import (
|
||||||
BaseSchema,
|
BaseSchema,
|
||||||
|
@ -66,7 +67,7 @@ class Album(Item):
|
||||||
default=list,
|
default=list,
|
||||||
)
|
)
|
||||||
genre = jsondata.ArrayField(
|
genre = jsondata.ArrayField(
|
||||||
verbose_name=_("genre"),
|
verbose_name=pgettext_lazy("music", "genre"),
|
||||||
base_field=models.CharField(blank=True, default="", max_length=50),
|
base_field=models.CharField(blank=True, default="", max_length=50),
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
{% if request.user.is_authenticated and not mark.shelf_type %}
|
{% if request.user.is_authenticated and not mark.shelf_type %}
|
||||||
<div id="item-primary-action" class="right mark">
|
<div id="item-primary-action" class="right mark">
|
||||||
<div class="item-action item-mark-buttons">
|
<div class="item-action item-mark-buttons">
|
||||||
{% for k, v in shelf_labels %}
|
{% for k, v in shelf_actions %}
|
||||||
{% if v and k != 'dropped' %}
|
{% if v and k != 'dropped' %}
|
||||||
<button class="primary"
|
<button class="primary"
|
||||||
data-status="{{ k }}"
|
data-status="{{ k }}"
|
||||||
|
|
|
@ -17,9 +17,9 @@ from journal.models import (
|
||||||
Comment,
|
Comment,
|
||||||
Mark,
|
Mark,
|
||||||
Review,
|
Review,
|
||||||
|
ShelfManager,
|
||||||
ShelfMember,
|
ShelfMember,
|
||||||
ShelfType,
|
ShelfType,
|
||||||
get_shelf_labels_for_category,
|
|
||||||
q_piece_in_home_feed_of_user,
|
q_piece_in_home_feed_of_user,
|
||||||
q_piece_visible_to_user,
|
q_piece_visible_to_user,
|
||||||
)
|
)
|
||||||
|
@ -94,7 +94,7 @@ def retrieve(request, item_path, item_uuid):
|
||||||
my_collections = []
|
my_collections = []
|
||||||
collection_list = []
|
collection_list = []
|
||||||
child_item_comments = []
|
child_item_comments = []
|
||||||
shelf_labels = get_shelf_labels_for_category(item.category)
|
shelf_actions = ShelfManager.get_actions_for_category(item.category)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
visible = q_piece_visible_to_user(request.user)
|
visible = q_piece_visible_to_user(request.user)
|
||||||
mark = Mark(request.user.identity, item)
|
mark = Mark(request.user.identity, item)
|
||||||
|
@ -128,7 +128,7 @@ def retrieve(request, item_path, item_uuid):
|
||||||
"child_item_comments": child_item_comments,
|
"child_item_comments": child_item_comments,
|
||||||
"my_collections": my_collections,
|
"my_collections": my_collections,
|
||||||
"collection_list": collection_list,
|
"collection_list": collection_list,
|
||||||
"shelf_labels": shelf_labels,
|
"shelf_actions": shelf_actions,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
@ -18,6 +19,11 @@ def all_categories():
|
||||||
return item_categories()
|
return item_categories()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def all_languages():
|
||||||
|
return settings.LANGUAGES
|
||||||
|
|
||||||
|
|
||||||
@register.filter(is_safe=True)
|
@register.filter(is_safe=True)
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def duration_format(value, unit):
|
def duration_format(value, unit):
|
||||||
|
|
|
@ -17,15 +17,7 @@ from .mark import Mark
|
||||||
from .rating import Rating
|
from .rating import Rating
|
||||||
from .renderers import render_md
|
from .renderers import render_md
|
||||||
from .review import Review
|
from .review import Review
|
||||||
from .shelf import (
|
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
|
||||||
SHELF_LABELS,
|
|
||||||
Shelf,
|
|
||||||
ShelfLogEntry,
|
|
||||||
ShelfManager,
|
|
||||||
ShelfMember,
|
|
||||||
ShelfType,
|
|
||||||
get_shelf_labels_for_category,
|
|
||||||
)
|
|
||||||
from .tag import Tag, TagManager, TagMember
|
from .tag import Tag, TagManager, TagMember
|
||||||
from .utils import (
|
from .utils import (
|
||||||
journal_exists_for_item,
|
journal_exists_for_item,
|
||||||
|
|
|
@ -71,6 +71,21 @@ class Mark:
|
||||||
def action_label_for_feed(self) -> str:
|
def action_label_for_feed(self) -> str:
|
||||||
return str(self.action_label)
|
return str(self.action_label)
|
||||||
|
|
||||||
|
def get_action_for_feed(self, item_link=None):
|
||||||
|
if self.shelfmember and self.shelf_type:
|
||||||
|
tpl = ShelfManager.get_action_template(self.shelf_type, self.item.category)
|
||||||
|
elif self.comment:
|
||||||
|
tpl = ShelfManager.get_action_template(
|
||||||
|
ShelfType.PROGRESS, self.comment.item.category
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tpl = ""
|
||||||
|
if item_link:
|
||||||
|
i = f'<a href="{item_link}">{self.item.display_title}</a>'
|
||||||
|
else:
|
||||||
|
i = self.item.display_title
|
||||||
|
return _(tpl).format(item=i)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shelf_label(self) -> str | None:
|
def shelf_label(self) -> str | None:
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,6 @@ from functools import cached_property
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from markdownify import markdownify as md
|
from markdownify import markdownify as md
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
|
||||||
|
|
|
@ -24,44 +24,253 @@ class ShelfType(models.TextChoices):
|
||||||
DROPPED = ("dropped", _("DROPPED"))
|
DROPPED = ("dropped", _("DROPPED"))
|
||||||
|
|
||||||
|
|
||||||
SHELF_LABELS = [
|
_REVIEWED = "reviewed"
|
||||||
[ItemCategory.Book, ShelfType.WISHLIST, _("wants to read")],
|
|
||||||
[ItemCategory.Book, ShelfType.PROGRESS, _("started reading")],
|
_SHELF_LABELS = [
|
||||||
[ItemCategory.Book, ShelfType.COMPLETE, _("finished reading")],
|
[
|
||||||
[ItemCategory.Book, ShelfType.DROPPED, _("stopped reading")],
|
ItemCategory.Book,
|
||||||
[ItemCategory.Movie, ShelfType.WISHLIST, _("wants to watch")],
|
ShelfType.WISHLIST,
|
||||||
[ItemCategory.Movie, ShelfType.PROGRESS, _("started watching")],
|
_("books to read"),
|
||||||
[ItemCategory.Movie, ShelfType.COMPLETE, _("finished watching")],
|
_("want to read"),
|
||||||
[ItemCategory.Movie, ShelfType.DROPPED, _("stopped watching")],
|
_("wants to read {item}"),
|
||||||
[ItemCategory.TV, ShelfType.WISHLIST, _("wants to watch")],
|
],
|
||||||
[ItemCategory.TV, ShelfType.PROGRESS, _("started watching")],
|
[
|
||||||
[ItemCategory.TV, ShelfType.COMPLETE, _("finished watching")],
|
ItemCategory.Book,
|
||||||
[ItemCategory.TV, ShelfType.DROPPED, _("stopped watching")],
|
ShelfType.PROGRESS,
|
||||||
[ItemCategory.Music, ShelfType.WISHLIST, _("wants to listen")],
|
_("books reading"),
|
||||||
[ItemCategory.Music, ShelfType.PROGRESS, _("started listening")],
|
_("start reading"),
|
||||||
[ItemCategory.Music, ShelfType.COMPLETE, _("finished listening")],
|
_("started reading {item}"),
|
||||||
[ItemCategory.Music, ShelfType.DROPPED, _("stopped listening")],
|
],
|
||||||
[ItemCategory.Game, ShelfType.WISHLIST, _("wants to play")],
|
[
|
||||||
[ItemCategory.Game, ShelfType.PROGRESS, _("started playing")],
|
ItemCategory.Book,
|
||||||
[ItemCategory.Game, ShelfType.COMPLETE, _("finished playing")],
|
ShelfType.COMPLETE,
|
||||||
[ItemCategory.Game, ShelfType.DROPPED, _("stopped playing")],
|
_("books completed"),
|
||||||
[ItemCategory.Podcast, ShelfType.WISHLIST, _("wants to listen")],
|
_("finish reading"),
|
||||||
[ItemCategory.Podcast, ShelfType.PROGRESS, _("started listening")],
|
_("finished reading {item}"),
|
||||||
[ItemCategory.Podcast, ShelfType.COMPLETE, _("finished listening")],
|
],
|
||||||
[ItemCategory.Podcast, ShelfType.DROPPED, _("stopped listening")],
|
[
|
||||||
[ItemCategory.Performance, ShelfType.WISHLIST, _("wants to see")],
|
ItemCategory.Book,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("books dropped"),
|
||||||
|
_("stop reading"),
|
||||||
|
_("stopped reading {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Book,
|
||||||
|
_REVIEWED,
|
||||||
|
_("books reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("movies to watch"),
|
||||||
|
_("want to watch"),
|
||||||
|
_("wants to watch {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ShelfType.PROGRESS,
|
||||||
|
_("movies watching"),
|
||||||
|
_("start watching"),
|
||||||
|
_("started watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("movies watched"),
|
||||||
|
_("finish watching"),
|
||||||
|
_("finished watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("movies dropped"),
|
||||||
|
_("stop watching"),
|
||||||
|
_("stopped watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Movie,
|
||||||
|
_REVIEWED,
|
||||||
|
_("movies reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.TV,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("TV shows to watch"),
|
||||||
|
_("want to watch"),
|
||||||
|
_("wants to watch {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.TV,
|
||||||
|
ShelfType.PROGRESS,
|
||||||
|
_("TV shows watching"),
|
||||||
|
_("start watching"),
|
||||||
|
_("started watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.TV,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("TV shows watched"),
|
||||||
|
_("finish watching"),
|
||||||
|
_("finished watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.TV,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("TV shows dropped"),
|
||||||
|
_("stop watching"),
|
||||||
|
_("stopped watching {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.TV,
|
||||||
|
_REVIEWED,
|
||||||
|
_("TV shows reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Music,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("albums to listen"),
|
||||||
|
_("want to listen"),
|
||||||
|
_("wants to listen {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Music,
|
||||||
|
ShelfType.PROGRESS,
|
||||||
|
_("albums listening"),
|
||||||
|
_("start listening"),
|
||||||
|
_("started listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Music,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("albums to listen"),
|
||||||
|
_("finish listening"),
|
||||||
|
_("finished listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Music,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("albums dropped"),
|
||||||
|
_("stop listening"),
|
||||||
|
_("stopped listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Music,
|
||||||
|
_REVIEWED,
|
||||||
|
_("albums reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Game,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("games to play"),
|
||||||
|
_("want to play"),
|
||||||
|
_("wants to play {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Game,
|
||||||
|
ShelfType.PROGRESS,
|
||||||
|
_("games playing"),
|
||||||
|
_("start playing"),
|
||||||
|
_("started playing {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Game,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("games played"),
|
||||||
|
_("finish playing"),
|
||||||
|
_("finished playing {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Game,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("games dropped"),
|
||||||
|
_("stop playing"),
|
||||||
|
_("stopped playing {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Game,
|
||||||
|
_REVIEWED,
|
||||||
|
_("games reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("podcasts to listen"),
|
||||||
|
_("want to listen"),
|
||||||
|
_("wants to listen {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
ShelfType.PROGRESS,
|
||||||
|
_("podcasts listening"),
|
||||||
|
_("start listening"),
|
||||||
|
_("started listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("podcasts listened"),
|
||||||
|
_("finish listening"),
|
||||||
|
_("finished listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("podcasts dropped"),
|
||||||
|
_("stop listening"),
|
||||||
|
_("stopped listening {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
_REVIEWED,
|
||||||
|
_("podcasts reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Performance,
|
||||||
|
ShelfType.WISHLIST,
|
||||||
|
_("performances to see"),
|
||||||
|
_("want to see"),
|
||||||
|
_("wants to see {item}"),
|
||||||
|
],
|
||||||
# disable progress shelf for Performance
|
# disable progress shelf for Performance
|
||||||
[ItemCategory.Performance, ShelfType.PROGRESS, ""],
|
[ItemCategory.Performance, ShelfType.PROGRESS, "", "", ""],
|
||||||
[ItemCategory.Performance, ShelfType.COMPLETE, _("finished seeing")],
|
[
|
||||||
[ItemCategory.Performance, ShelfType.DROPPED, _("stopped seeing")],
|
ItemCategory.Performance,
|
||||||
|
ShelfType.COMPLETE,
|
||||||
|
_("performances saw"),
|
||||||
|
_("finish seeing"),
|
||||||
|
_("finished seeing {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Performance,
|
||||||
|
ShelfType.DROPPED,
|
||||||
|
_("performances dropped"),
|
||||||
|
_("stop seeing"),
|
||||||
|
_("stopped seeing {item}"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ItemCategory.Performance,
|
||||||
|
_REVIEWED,
|
||||||
|
_("performances reviewed"),
|
||||||
|
_("review"),
|
||||||
|
_("wrote a review of {item}"),
|
||||||
|
],
|
||||||
]
|
]
|
||||||
# grammatically problematic, for translation only
|
# grammatically problematic, for translation only
|
||||||
|
|
||||||
|
|
||||||
def get_shelf_labels_for_category(item_category: ItemCategory):
|
|
||||||
return [(n[1], n[2]) for n in SHELF_LABELS if n[0] == item_category]
|
|
||||||
|
|
||||||
|
|
||||||
class ShelfMember(ListMember):
|
class ShelfMember(ListMember):
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"Shelf", related_name="members", on_delete=models.CASCADE
|
"Shelf", related_name="members", on_delete=models.CASCADE
|
||||||
|
@ -284,23 +493,39 @@ class ShelfManager:
|
||||||
# )
|
# )
|
||||||
# return shelf.members.all().order_by
|
# return shelf.members.all().order_by
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_labels_for_category(cls, item_category: ItemCategory):
|
||||||
|
return [(n[1], n[2]) for n in _SHELF_LABELS if n[0] == item_category]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_actions_for_category(cls, item_category: ItemCategory):
|
||||||
|
return [
|
||||||
|
(n[1], n[3])
|
||||||
|
for n in _SHELF_LABELS
|
||||||
|
if n[0] == item_category and n[1] != _REVIEWED
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, shelf_type: ShelfType | str, item_category: ItemCategory) -> str:
|
||||||
|
st = str(shelf_type)
|
||||||
|
sts = [n[2] for n in _SHELF_LABELS if n[0] == item_category and n[1] == st]
|
||||||
|
return sts[0] if sts else st
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_action_label(
|
def get_action_label(
|
||||||
cls, shelf_type: ShelfType | str, item_category: ItemCategory
|
cls, shelf_type: ShelfType | str, item_category: ItemCategory
|
||||||
) -> str:
|
) -> str:
|
||||||
st = str(shelf_type)
|
st = str(shelf_type)
|
||||||
sts = [n[2] for n in SHELF_LABELS if n[0] == item_category and n[1] == st]
|
sts = [n[3] for n in _SHELF_LABELS if n[0] == item_category and n[1] == st]
|
||||||
return sts[0] if sts else st
|
return sts[0] if sts else st
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_label(cls, shelf_type: ShelfType, item_category: ItemCategory):
|
def get_action_template(
|
||||||
ic = ItemCategory(item_category).label
|
cls, shelf_type: ShelfType | str, item_category: ItemCategory
|
||||||
st = cls.get_action_label(shelf_type, item_category)
|
) -> str:
|
||||||
return (
|
st = str(shelf_type)
|
||||||
_("{shelf_label} {item_category}").format(shelf_label=st, item_category=ic)
|
sts = [n[4] for n in _SHELF_LABELS if n[0] == item_category and n[1] == st]
|
||||||
if st
|
return sts[0] if sts else st
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_manager_for_user(owner: APIdentity):
|
def get_manager_for_user(owner: APIdentity):
|
||||||
|
|
|
@ -23,11 +23,11 @@
|
||||||
<div>
|
<div>
|
||||||
<form method="post" action="{% url 'journal:mark' item.uuid %}">
|
<form method="post" action="{% url 'journal:mark' item.uuid %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if shelf_labels %}
|
{% if shelf_actions %}
|
||||||
<div class="grid mark-line">
|
<div class="grid mark-line">
|
||||||
<div>
|
<div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for k, v in shelf_labels %}
|
{% for k, v in shelf_actions %}
|
||||||
{% if v %}
|
{% if v %}
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name="status"
|
name="status"
|
||||||
|
|
|
@ -102,7 +102,9 @@ def render_list(
|
||||||
page_number = int(request.GET.get("page", default=1))
|
page_number = int(request.GET.get("page", default=1))
|
||||||
members = paginator.get_page(page_number)
|
members = paginator.get_page(page_number)
|
||||||
pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET)
|
pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET)
|
||||||
shelf_labels = get_shelf_labels_for_category(item_category) if item_category else []
|
shelf_labels = (
|
||||||
|
ShelfManager.get_labels_for_category(item_category) if item_category else []
|
||||||
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"user_{type}_list.html",
|
f"user_{type}_list.html",
|
||||||
|
|
|
@ -15,7 +15,7 @@ from common.utils import AuthedHttpRequest, get_uuid_or_404
|
||||||
from mastodon.api import boost_toot_later, share_comment
|
from mastodon.api import boost_toot_later, share_comment
|
||||||
from takahe.utils import Takahe
|
from takahe.utils import Takahe
|
||||||
|
|
||||||
from ..models import Comment, Mark, ShelfType, TagManager, get_shelf_labels_for_category
|
from ..models import Comment, Mark, ShelfManager, ShelfType, TagManager
|
||||||
from .common import render_list, render_relogin, target_identity_required
|
from .common import render_list, render_relogin, target_identity_required
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
@ -45,7 +45,7 @@ def mark(request: AuthedHttpRequest, item_uuid):
|
||||||
mark = Mark(request.user.identity, item)
|
mark = Mark(request.user.identity, item)
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
tags = request.user.identity.tag_manager.get_item_tags(item)
|
tags = request.user.identity.tag_manager.get_item_tags(item)
|
||||||
shelf_labels = get_shelf_labels_for_category(item.category)
|
shelf_actions = ShelfManager.get_actions_for_category(item.category)
|
||||||
shelf_type = request.GET.get("shelf_type", mark.shelf_type)
|
shelf_type = request.GET.get("shelf_type", mark.shelf_type)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
@ -55,7 +55,7 @@ def mark(request: AuthedHttpRequest, item_uuid):
|
||||||
"mark": mark,
|
"mark": mark,
|
||||||
"shelf_type": shelf_type,
|
"shelf_type": shelf_type,
|
||||||
"tags": ",".join(tags),
|
"tags": ",".join(tags),
|
||||||
"shelf_labels": shelf_labels,
|
"shelf_actions": shelf_actions,
|
||||||
"date_today": timezone.localdate().isoformat(),
|
"date_today": timezone.localdate().isoformat(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -70,9 +70,7 @@ def profile(request: AuthedHttpRequest, user_name):
|
||||||
.order_by("-created_time")
|
.order_by("-created_time")
|
||||||
)
|
)
|
||||||
shelf_list[category]["reviewed"] = {
|
shelf_list[category]["reviewed"] = {
|
||||||
"title": _("{shelf_label} {item_category}").format(
|
"title": target.shelf_manager.get_label("reviewed", category),
|
||||||
shelf_label="reviewed", item_category=category.label
|
|
||||||
),
|
|
||||||
"count": reviews.count(),
|
"count": reviews.count(),
|
||||||
"members": reviews[:10].prefetch_related("item"),
|
"members": reviews[:10].prefetch_related("item"),
|
||||||
}
|
}
|
||||||
|
|
2301
locale/zh_Hans/LC_MESSAGES/django.po
Normal file
2301
locale/zh_Hans/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -520,8 +520,11 @@ def share_comment(comment):
|
||||||
if user.preference.mastodon_append_tag
|
if user.preference.mastodon_append_tag
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
action = ShelfManager.get_action_label(ShelfType.PROGRESS, comment.item.category)
|
tpl = ShelfManager.get_action_template(ShelfType.PROGRESS, comment.item.category)
|
||||||
content = f"{action} {comment.item.display_title}\n{comment.text}\n{comment.item.absolute_url}{tags}"
|
content = (
|
||||||
|
_(tpl).format(item=comment.item.display_title)
|
||||||
|
+ f"\n{comment.text}\n{comment.item.absolute_url}{tags}"
|
||||||
|
)
|
||||||
update_id = None
|
update_id = None
|
||||||
if comment.metadata.get(
|
if comment.metadata.get(
|
||||||
"shared_link"
|
"shared_link"
|
||||||
|
@ -562,7 +565,9 @@ def share_mark(mark, post_as_new=False):
|
||||||
site.star_mode if site else 0,
|
site.star_mode if site else 0,
|
||||||
)
|
)
|
||||||
spoiler_text, txt = get_spoiler_text(mark.comment_text or "", mark.item)
|
spoiler_text, txt = get_spoiler_text(mark.comment_text or "", mark.item)
|
||||||
content = f"{mark.action_label_for_feed}《{mark.item.display_title}》{stars}\n{mark.item.absolute_url}\n{txt}{tags}"
|
content = (
|
||||||
|
f"{mark.get_action_for_feed()} {stars}\n{mark.item.absolute_url}\n{txt}{tags}"
|
||||||
|
)
|
||||||
update_id = (
|
update_id = (
|
||||||
None
|
None
|
||||||
if post_as_new
|
if post_as_new
|
||||||
|
@ -590,6 +595,7 @@ def share_mark(mark, post_as_new=False):
|
||||||
|
|
||||||
def share_review(review):
|
def share_review(review):
|
||||||
from catalog.common import ItemCategory
|
from catalog.common import ItemCategory
|
||||||
|
from journal.models import ShelfManager
|
||||||
|
|
||||||
user = review.owner.user
|
user = review.owner.user
|
||||||
visibility = get_toot_visibility(review.visibility, user)
|
visibility = get_toot_visibility(review.visibility, user)
|
||||||
|
@ -601,9 +607,11 @@ def share_review(review):
|
||||||
if user.preference.mastodon_append_tag
|
if user.preference.mastodon_append_tag
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
tpl = ShelfManager.get_action_template("reviewed", review.item.category)
|
||||||
content = (
|
content = (
|
||||||
"wrote a review of {item_title}".format(item_title=review.item.display_title)
|
_(tpl).format(item=review.item.display_title)
|
||||||
+ "\n{review.title}\n{review.absolute_url}{tags}"
|
+ "\n{review.title}\n{review.absolute_url} "
|
||||||
|
+ tags
|
||||||
)
|
)
|
||||||
update_id = None
|
update_id = None
|
||||||
if review.metadata.get(
|
if review.metadata.get(
|
||||||
|
|
|
@ -6,4 +6,4 @@ djlint~=1.34.0
|
||||||
isort~=5.12.0
|
isort~=5.12.0
|
||||||
lxml-stubs
|
lxml-stubs
|
||||||
pre-commit
|
pre-commit
|
||||||
pyright==1.1.344
|
pyright==1.1.350
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<div class="spacing">
|
<div class="spacing">
|
||||||
写了评论
|
{% blocktrans with item=activity.action_object.item.title %}wrote a review of {{item}}{% endblocktrans %}
|
||||||
<a href="{{ activity.action_object.url }}">{{ activity.action_object.title }}</a>
|
<a href="{{ activity.action_object.url }}">{{ activity.action_object.title }}</a>
|
||||||
{% if activity.action_object.mark.rating_grade %}
|
{% if activity.action_object.mark.rating_grade %}
|
||||||
{{ activity.action_object.mark.rating_grade | rating_star }}
|
{{ activity.action_object.mark.rating_grade | rating_star }}
|
||||||
|
|
|
@ -591,11 +591,14 @@ class Takahe:
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{comment.item_url}"
|
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{comment.item_url}"
|
||||||
action = ShelfManager.get_action_label(
|
tpl = ShelfManager.get_action_template(
|
||||||
ShelfType.PROGRESS, comment.item.category
|
ShelfType.PROGRESS, comment.item.category
|
||||||
)
|
)
|
||||||
pre_conetent = (
|
pre_conetent = (
|
||||||
f'{action} <a href="{item_link}">{comment.item.display_title}</a><br>'
|
_(tpl).format(
|
||||||
|
item=f'<a href="{item_link}">{comment.item.display_title}</a>'
|
||||||
|
)
|
||||||
|
+ "<br>"
|
||||||
)
|
)
|
||||||
spoiler, txt = Takahe.get_spoiler_text(comment.text, comment.item)
|
spoiler, txt = Takahe.get_spoiler_text(comment.text, comment.item)
|
||||||
content = f"{txt}\n{tags}"
|
content = f"{txt}\n{tags}"
|
||||||
|
@ -626,6 +629,7 @@ class Takahe:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post_review(review, share_as_new_post: bool) -> Post | None:
|
def post_review(review, share_as_new_post: bool) -> Post | None:
|
||||||
from catalog.common import ItemCategory
|
from catalog.common import ItemCategory
|
||||||
|
from journal.models import ShelfManager
|
||||||
|
|
||||||
user = review.owner.user
|
user = review.owner.user
|
||||||
tags = (
|
tags = (
|
||||||
|
@ -639,10 +643,9 @@ class Takahe:
|
||||||
stars = _rating_to_emoji(review.rating_grade, 1)
|
stars = _rating_to_emoji(review.rating_grade, 1)
|
||||||
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{review.item.url}"
|
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{review.item.url}"
|
||||||
|
|
||||||
|
tpl = ShelfManager.get_action_template("reviewed", review.item.category)
|
||||||
pre_conetent = (
|
pre_conetent = (
|
||||||
"wrote a review of {item_title}".format(
|
_(tpl).format(item=f'<a href="{item_link}">{review.item.display_title}</a>')
|
||||||
item_title=f'<a href="{item_link}">{review.item.display_title}</a>'
|
|
||||||
)
|
|
||||||
+ f'<br><a href="{review.absolute_url}">{review.title}</a>'
|
+ f'<br><a href="{review.absolute_url}">{review.title}</a>'
|
||||||
)
|
)
|
||||||
content = f"{stars}\n{tags}"
|
content = f"{stars}\n{tags}"
|
||||||
|
@ -685,7 +688,7 @@ class Takahe:
|
||||||
)
|
)
|
||||||
stars = _rating_to_emoji(mark.rating_grade, 1)
|
stars = _rating_to_emoji(mark.rating_grade, 1)
|
||||||
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{mark.item.url}"
|
item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{mark.item.url}"
|
||||||
pre_conetent = f'{mark.action_label_for_feed} <a href="{item_link}">{mark.item.display_title}</a>'
|
pre_conetent = mark.get_action_for_feed(item_link=item_link)
|
||||||
spoiler, txt = Takahe.get_spoiler_text(mark.comment_text, mark.item)
|
spoiler, txt = Takahe.get_spoiler_text(mark.comment_text, mark.item)
|
||||||
content = f"{stars} \n{txt}\n{tags}"
|
content = f"{stars} \n{txt}\n{tags}"
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -57,6 +57,11 @@ def preferences(request):
|
||||||
"hidden_categories",
|
"hidden_categories",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
lang = request.POST.get("language")
|
||||||
|
print(lang)
|
||||||
|
if lang in dict(settings.LANGUAGES).keys() and lang != request.user.language:
|
||||||
|
request.user.language = lang
|
||||||
|
request.user.save(update_fields=["language"])
|
||||||
clear_preference_cache(request)
|
clear_preference_cache(request)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
16
users/middlewares.py
Normal file
16
users/middlewares.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import translation
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageMiddleware(MiddlewareMixin):
|
||||||
|
def process_request(self, request):
|
||||||
|
user_language = settings.LANGUAGE_CODE
|
||||||
|
user = getattr(request, "user", None)
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
user_language = getattr(user, "language", "")
|
||||||
|
if user_language not in dict(settings.LANGUAGES).keys():
|
||||||
|
user_language = settings.LANGUAGE_CODE
|
||||||
|
current_language = translation.get_language()
|
||||||
|
if user_language != current_language:
|
||||||
|
translation.activate(user_language)
|
23
users/migrations/0020_user_language.py
Normal file
23
users/migrations/0020_user_language.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-04-04 01:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0019_task"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="language",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("en", "English"), ("zh-hans", "Simplified Chinese")],
|
||||||
|
default="en",
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="language",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,6 +4,7 @@ from functools import cached_property
|
||||||
from typing import TYPE_CHECKING, ClassVar
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser, BaseUserManager
|
from django.contrib.auth.models import AbstractUser, BaseUserManager
|
||||||
from django.contrib.auth.validators import UnicodeUsernameValidator
|
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -91,6 +92,13 @@ class User(AbstractUser):
|
||||||
pending_email = models.EmailField(
|
pending_email = models.EmailField(
|
||||||
_("email address pending verification"), default=None, null=True
|
_("email address pending verification"), default=None, null=True
|
||||||
)
|
)
|
||||||
|
language = models.CharField(
|
||||||
|
_("language"),
|
||||||
|
max_length=10,
|
||||||
|
choices=settings.LANGUAGES,
|
||||||
|
null=False,
|
||||||
|
default="en",
|
||||||
|
)
|
||||||
local_following = models.ManyToManyField(
|
local_following = models.ManyToManyField(
|
||||||
through="Follow",
|
through="Follow",
|
||||||
to="self",
|
to="self",
|
||||||
|
@ -431,6 +439,8 @@ class User(AbstractUser):
|
||||||
from .preference import Preference
|
from .preference import Preference
|
||||||
|
|
||||||
new_user = cls(**param)
|
new_user = cls(**param)
|
||||||
|
if "language" not in param:
|
||||||
|
new_user.language = settings.LANGUAGE_CODE
|
||||||
new_user.save()
|
new_user.save()
|
||||||
Preference.objects.create(user=new_user)
|
Preference.objects.create(user=new_user)
|
||||||
if new_user.username: # TODO make username required in registeration
|
if new_user.username: # TODO make username required in registeration
|
||||||
|
|
|
@ -134,6 +134,16 @@
|
||||||
placeholder="例如 #我的书影音"
|
placeholder="例如 #我的书影音"
|
||||||
value="{{ request.user.preference.mastodon_append_tag }}">
|
value="{{ request.user.preference.mastodon_append_tag }}">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
{% all_languages as languages %}
|
||||||
|
<label>{% trans 'language' %}</label>
|
||||||
|
<select name="language">
|
||||||
|
{% for lang in languages %}
|
||||||
|
<option value="{{ lang.0 }}"
|
||||||
|
{% if lang.0 == request.user.language %}selected{% endif %}>{{ lang.1 }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans '搜索时不显示以下类型:' %}</legend>
|
<legend>{% trans '搜索时不显示以下类型:' %}</legend>
|
||||||
<select name="hidden_categories" size="3" multiple>
|
<select name="hidden_categories" size="3" multiple>
|
||||||
|
|
Loading…
Add table
Reference in a new issue