From 3e84f973fa49aa8157e69de0ba3109f8c2e915a1 Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Tue, 21 Jan 2025 18:41:27 +0700
Subject: [PATCH 1/6] marked missing strings for translation

---
 frontend/html/admin/report.html               | 17 ++++++----
 frontend/html/posts/show/layout.html          | 18 +++++-----
 .../static/js/components/PostBookmark.vue     |  2 +-
 misc/forms.py                                 |  9 ++---
 posts/forms/admin.py                          | 33 ++++++++++---------
 posts/views/admin.py                          |  9 ++---
 6 files changed, 48 insertions(+), 40 deletions(-)

diff --git a/frontend/html/admin/report.html b/frontend/html/admin/report.html
index e6638a8..f998650 100644
--- a/frontend/html/admin/report.html
+++ b/frontend/html/admin/report.html
@@ -1,19 +1,20 @@
 {% extends "layout.html" %}
+{% load i18n %}
 
 {% block title %}
-    Для модератора — {{ block.super }}
+    {% translate "Для модератора" %} — {{ block.super }}
 {% endblock %}
 
 {% block content %}
     <div class="content compose">
-        <div class="content-header">Сообщение модератору</div>
+        <div class="content-header">{% translate "Сообщение модератору" %}</div>
         {% if not submitted %}
         <div class="content-description content-description-left">
             <p>
-                Если вы здесь, то наверное в сообществе случилось что-то неприятное.
+                {% translate "Если вы здесь, то наверное в сообществе случилось что-то неприятное." %}
             </p>
             <p>
-                Расскажите нам все подробности, и это обращение обязательно увидят модераторы.
+                {% translate "Расскажите нам все подробности, и это обращение обязательно увидят модераторы." %}
             </p>
         </div>
 
@@ -25,8 +26,10 @@
                     <label for="{{ form.text.id_for_label }}" class="form-label">{{ form.text.label }}</label>
                     {{ form.text }}
                     <span class="form-row-help form-row-help-wide">
+                        {% blocktranslate %}
                         Можно использовать <a href="https://www.markdownguide.org/basic-syntax/" target="_blank">Markdown</a>.
                         Для загрузки картинок просто перетащите их в редактор.
+                        {% endblocktranslate %}
                     </span>
                 </div>
 
@@ -40,7 +43,7 @@
 
                 <div class="form-row form-row-space-between">
                     <button type="submit" class="button">
-                        Отправить
+                        {% translate "Отправить" %}
                     </button>
                 </div>
 
@@ -49,10 +52,10 @@
         {% else %}
         <div class="content-description content-description-left">
             <p>
-                Спасибо! Мы получили ваше сообщение.
+                {% translate "Спасибо! Мы получили ваше сообщение." %}
             </p>
             <p>
-                Здоровое сообщество очень важно для нас и мы обязательно что-нибудь предпримем.
+                {% translate "Здоровое сообщество очень важно для нас и мы обязательно что-нибудь предпримем." %}
             </p>
         </div>
         {% endif %}
diff --git a/frontend/html/posts/show/layout.html b/frontend/html/posts/show/layout.html
index 812355d..0003121 100644
--- a/frontend/html/posts/show/layout.html
+++ b/frontend/html/posts/show/layout.html
@@ -46,19 +46,19 @@
                     <div class="post-comments-title-left">
                         {% if post.comment_count > 0 %}
                             <a href="#comments" class="post-comments-count">
-                                {{ post.comment_count }}  {% translate "комментариев" %} 👇
+                                {{ post.comment_count }}  {% translate "комментариев" %}&nbsp;👇
                             </a>
 
                             <form action=".#comments" method="get" class="post-comments-order">
                                 <select name="comment_order" onchange="this.form.submit()">
-                                    <option value="-upvotes" {% if comment_order == "-upvotes" %}selected{% endif %}>по крутости</option>
-                                    <option value="-created_at" {% if comment_order == "-created_at" %}selected{% endif %}>по свежести</option>
-                                    <option value="created_at" {% if comment_order == "created_at" %}selected{% endif %}>по порядку</option>
+                                    <option value="-upvotes" {% if comment_order == "-upvotes" %}selected{% endif %}>{% translate "по крутости" %}</option>
+                                    <option value="-created_at" {% if comment_order == "-created_at" %}selected{% endif %}>{% translate "по свежести" %}</option>
+                                    <option value="created_at" {% if comment_order == "created_at" %}selected{% endif %}>{% translate "по порядку" %}</option>
                                 </select>
                             </form>
                         {% elif post.is_commentable %}
                             <a href="#comments" class="post-comments-count">
-                                Откомментируйте первым 👇
+                                {% translate "Откомментируйте первым" %}&nbsp;👇
                             </a>
                         {% endif %}
                     </div>
@@ -70,7 +70,7 @@
                                 {% if subscription %}is-active-by-default{% endif %}
                                 class="post-comments-subscription"
                             >
-                                подписка на комментарии
+                            {% translate "подписка на комментарии" %}
                             </post-subscription>
                         {% endif %}
                     </div>
@@ -89,9 +89,11 @@
 
                     <div class="post-comments-rules">
                         <ul>
-                            <li>Помните, что за оскорбление других участников, пассивную агрессию, недружелюбность и прочие нарушения <!--<a href="{% url "docs" "about" %}">-->правил этикета<!--</a>--> — будет бан.</li>
-                            <li>Если кто-то ведёт себя плохо — скажите им. Если они продолжают, можно <a href="{% url "report" post.slug %}">пожаловаться</a>.</li>
+                            {% blocktranslate with post.slug|report_url as report_url %}
+                            <li>Помните, что за оскорбление других участников, пассивную агрессию, недружелюбность и прочие нарушения правил этикета — будет бан.</li>
+                            <li>Если кто-то ведёт себя плохо — скажите им. Если они продолжают, можно <a href="{{ report_url }}">пожаловаться</a>.</li>
                             <li>Можно использовать <a href="https://www.markdownguide.org/basic-syntax/" target="_blank">Markdown</a>.</li>
+                            {% endblocktranslate %}
                         </ul>
                     </div>
                 {% endif %}
diff --git a/frontend/static/js/components/PostBookmark.vue b/frontend/static/js/components/PostBookmark.vue
index 16891de..351f62b 100644
--- a/frontend/static/js/components/PostBookmark.vue
+++ b/frontend/static/js/components/PostBookmark.vue
@@ -1,7 +1,7 @@
 <template>
     <a :href="bookmarkUrl" @click.prevent="toggle">
         <span v-if="isLoading">🤔</span>
-        <span v-if="isBookmarked"><i class="fas fa-bookmark"></i>&nbsp;Убрать из закладок</span>
+        <span v-if="isBookmarked"><i class="fas fa-bookmark"></i>&nbsp;{{ $gettext('Убрать из закладок') }}</span>
         <span v-else><i class="far fa-bookmark"></i>&nbsp;{{ $gettext('В закладки') }}</span>
     </a>
 </template>
diff --git a/misc/forms.py b/misc/forms.py
index 5cb32f0..33751fc 100644
--- a/misc/forms.py
+++ b/misc/forms.py
@@ -1,22 +1,23 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 
 class UserReportForm(forms.Form):
     text = forms.CharField(
-        label="Текст обращения",
+        label=_("Текст обращения"),
         required=True,
         max_length=5000,
         widget=forms.Textarea(
             attrs={
                 "maxlength": 5000,
                 "class": "markdown-editor-full",
-                "placeholder": "Расскажите, что тут случилось?"
+                "placeholder": _("Расскажите, что тут случилось?")
             }
         ),
     )
     post = forms.SlugField(
         widget=forms.HiddenInput(),
-        label="Пост из жалобы", max_length=250)
+        label=_("Пост из жалобы"), max_length=250)
     user = forms.SlugField(
         widget=forms.HiddenInput(),
-        label="Автор жалобы", max_length=250)
+        label=_("Автор жалобы"), max_length=250)
diff --git a/posts/forms/admin.py b/posts/forms/admin.py
index b334229..ce663ff 100644
--- a/posts/forms/admin.py
+++ b/posts/forms/admin.py
@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 from common.data.labels import LABELS
 from posts.models.post import Post
@@ -6,79 +7,79 @@ from posts.models.post import Post
 
 class PostCuratorForm(forms.Form):
     change_type = forms.ChoiceField(
-        label="Сменить тип поста",
+        label=_("Сменить тип поста"),
         choices=[(None, "---")] + Post.TYPES,
         required=False,
     )
 
     new_label = forms.ChoiceField(
-        label="Выдать лейбл",
+        label=_("Выдать лейбл"),
         choices=[(None, "---")] + [(key, value.get("title")) for key, value in LABELS.items()],
         required=False,
     )
 
     remove_label = forms.BooleanField(
-        label="Удалить текуший лейбл",
+        label=_("Удалить текуший лейбл"),
         required=False
     )
 
     add_pin = forms.BooleanField(
-        label="Запинить",
+        label=_("Запинить"),
         required=False
     )
 
     pin_days = forms.IntegerField(
-        label="На сколько дней пин?",
+        label=_("На сколько дней пин?"),
         initial=1,
         required=False
     )
 
     remove_pin = forms.BooleanField(
-        label="Отпинить обратно",
+        label=_("Отпинить обратно"),
         required=False
     )
 
     move_up = forms.BooleanField(
-        label="Подбросить на главной",
+        label=_("Подбросить на главной"),
         required=False
     )
 
     move_down = forms.BooleanField(
-        label="Опустить на главной",
+        label=_("Опустить на главной"),
         required=False
     )
 
     shadow_ban = forms.BooleanField(
-        label="Шадоу бан (редко!)",
+        label=_("Шадоу бан (редко!)"),
         required=False,
     )
 
     hide_from_feeds = forms.BooleanField(
-        label="Скрыть с главной",
+        label=_("Скрыть с главной"),
         required=False,
     )
 
 
 class PostAdminForm(PostCuratorForm):
     toggle_is_commentable = forms.BooleanField(
-        label="Закрыть комменты (повторный клик переоткроет заново)",
+        label=_("Закрыть комменты (повторный клик переоткроет заново)"),
         required=False,
     )
 
     transfer_ownership = forms.CharField(
-        label="Передать владение постом другому юзернейму",
+        label=_("Передать владение постом другому юзернейму"),
         required=False,
     )
 
     refresh_linked = forms.BooleanField(
-        label="Обновить связанные посты",
+        label=_("Обновить связанные посты"),
         required=False,
     )
 
 
 class PostAnnounceForm(forms.Form):
     text = forms.CharField(
-        label="Текст анонса",
+        label=_("Текст анонса"),
         required=True,
         max_length=500000,
         widget=forms.Textarea(
@@ -88,11 +89,11 @@ class PostAnnounceForm(forms.Form):
         ),
     )
     image = forms.CharField(
-        label="Картинка",
+        label=_("Картинка"),
         required=False,
     )
     with_image = forms.BooleanField(
-        label="Постим с картинкой?",
+        label=_("Постим с картинкой?"),
         required=False,
         initial=True,
     )
diff --git a/posts/views/admin.py b/posts/views/admin.py
index 077abb6..86d6db2 100644
--- a/posts/views/admin.py
+++ b/posts/views/admin.py
@@ -1,4 +1,5 @@
 from django.shortcuts import get_object_or_404, render
+from django.utils.translation import gettext_lazy as _
 
 from auth.helpers import auth_required, moderator_role_required, curator_role_required
 from notifications.telegram.common import render_html_message
@@ -22,7 +23,7 @@ def curate_post(request, post_slug):
         form = PostCuratorForm()
 
     return render(request, "admin/simple_form.html", {
-        "title": "Курирование поста",
+        "title": _("Курирование поста"),
         "post": post,
         "form": form
     })
@@ -41,7 +42,7 @@ def admin_post(request, post_slug):
         form = PostAdminForm()
 
     return render(request, "admin/simple_form.html", {
-        "title": "Админить пост",
+        "title": _("Админить пост"),
         "post": post,
         "form": form
     })
@@ -66,13 +67,13 @@ def announce_post(request, post_slug):
                 image=form.cleaned_data["image"] if form.cleaned_data["with_image"] else None
             )
             return render(request, "message.html", {
-                "title": "Материал анонсирован!"
+                "title": _("Материал анонсирован!")
             })
     else:
         form = PostAnnounceForm(initial=initial)
 
     return render(request, "admin/simple_form.html", {
-        "title": "Анонсировать материал на канале",
+        "title": _("Анонсировать материал на канале"),
         "post": post,
         "form": form
     })
-- 
GitLab


From 557927147273f88830d6ca808300e60f5e24b0cd Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Tue, 21 Jan 2025 18:42:01 +0700
Subject: [PATCH 2/6] added 'report post' url template tag

---
 posts/templatetags/posts.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/posts/templatetags/posts.py b/posts/templatetags/posts.py
index c78e20f..2be2bac 100644
--- a/posts/templatetags/posts.py
+++ b/posts/templatetags/posts.py
@@ -128,3 +128,8 @@ def og_image(post):
     })
 
     return f"{settings.OG_IMAGE_GENERATOR_URL}?{params}"
+
+
+@register.filter
+def report_url(slug):
+    return reverse("report", args=[slug])
-- 
GitLab


From b98d7844adb14307d898f9c3bcfa7203665e63e0 Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Tue, 21 Jan 2025 18:43:28 +0700
Subject: [PATCH 3/6] added feature flag for timed membership

---
 auth/helpers.py                 |  9 ++++++---
 auth/models.py                  |  6 +++++-
 club/features.py                |  5 +++++
 club/settings.py                |  6 ++++++
 frontend/html/admin/invite.html | 34 +++++++++++++++++++++++++++++++++
 frontend/html/users/admin.html  |  2 ++
 landing/forms.py                |  7 +++++--
 landing/views.py                | 19 +++++++++++-------
 users/models/user.py            |  5 ++++-
 9 files changed, 79 insertions(+), 14 deletions(-)
 create mode 100644 frontend/html/admin/invite.html

diff --git a/auth/helpers.py b/auth/helpers.py
index d7906e1..555c5db 100644
--- a/auth/helpers.py
+++ b/auth/helpers.py
@@ -5,7 +5,7 @@ import jwt
 from django.shortcuts import redirect, render, get_object_or_404
 
 from auth.models import Session, Apps
-from club import settings
+from club import features, settings
 from club.exceptions import AccessDenied, ApiAuthRequired, ClubException, ApiException
 from posts.models.post import Post
 from users.models.user import User
@@ -111,7 +111,7 @@ def check_user_permissions(request, **context):
             and not request.path.startswith("/about/") \
             and not request.path.startswith("/messages/"):
 
-        if request.me and request.me.membership_expires_at < datetime.utcnow():
+        if request.me and features.MEMBERSHIP_ENABLED and request.me.membership_expires_at < datetime.utcnow():
             log.info("User membership expired.")
             return render(request, "auth/membership_expired.html", context)
 
@@ -198,10 +198,13 @@ def auth_switch(yes, no):
 
 
 def set_session_cookie(response, user, session):
+    expires_at = datetime.utcnow() + timedelta(days=30)
+    if features.MEMBERSHIP_ENABLED:
+        expires_at = max(user.membership_expires_at, expires_at)
     response.set_cookie(
         key="token",
         value=session.token,
-        expires=max(user.membership_expires_at, datetime.utcnow() + timedelta(days=30)),
+        expires=expires_at,
         httponly=True,
         secure=not settings.DEBUG,
     )
diff --git a/auth/models.py b/auth/models.py
index f74ea57..e1240b3 100644
--- a/auth/models.py
+++ b/auth/models.py
@@ -5,6 +5,7 @@ from django.conf import settings
 from django.contrib.postgres.fields import ArrayField
 from django.db import models
 
+from club import features
 from club.exceptions import RateLimitException, InvalidCode
 from users.models.user import User
 from utils.strings import random_string, random_number
@@ -40,11 +41,14 @@ class Session(models.Model):
 
     @classmethod
     def create_for_user(cls, user):
+        expires_at = datetime.utcnow() + timedelta(days=30)
+        if features.MEMBERSHIP_ENABLED:
+            expires_at = max(user.membership_expires_at, expires_at)
         return Session.objects.create(
             user=user,
             token=random_string(length=32),
             created_at=datetime.utcnow(),
-            expires_at=max(user.membership_expires_at, datetime.utcnow() + timedelta(days=30)),
+            expires_at=expires_at,
         )
 
 
diff --git a/club/features.py b/club/features.py
index 4918a53..14479d6 100644
--- a/club/features.py
+++ b/club/features.py
@@ -6,3 +6,8 @@
 #   True — feed is only visible to club members, other users will be redirected to landing page
 #   False — everyone can view the feed, it becomes the main page
 PRIVATE_FEED = False
+
+# Enable/disable timed membership for users
+#  True — users can buy a membership for a certain period of time
+#  False — membership is disabled, all users have access to all features
+MEMBERSHIP_ENABLED = False
diff --git a/club/settings.py b/club/settings.py
index 0f6eac8..4b44a23 100644
--- a/club/settings.py
+++ b/club/settings.py
@@ -177,6 +177,10 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
 EMAIL_USE_TLS = True
 DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", EMAIL_HOST_USER)
 
+if DEBUG:
+    EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
+    EMAIL_FILE_PATH = os.path.join(BASE_DIR, "emails")
+
 # These emails will receive notifications on new help requests
 HELP_REQUESTS_NOTIFICATION_RECIPIENTS = set(os.getenv("HELP_REQUESTS_NOTIFICATION_RECIPIENTS", "").split(","))
 
@@ -277,6 +281,8 @@ CLEARED_POST_TEXT = _("```\n" \
     "Если вы хотите приютить и развить эту тему как новый автор, напишите модераторам: moderator@plus7toolkit.com." \
     "\n```")
 
+DEFAULT_MEMBERSHIP_DAYS = 365  # days
+
 MODERATOR_USERNAME = "moderator"
 DELETED_USERNAME = "dev"
 
diff --git a/frontend/html/admin/invite.html b/frontend/html/admin/invite.html
new file mode 100644
index 0000000..437379a
--- /dev/null
+++ b/frontend/html/admin/invite.html
@@ -0,0 +1,34 @@
+{% extends "layout.html" %}
+{% load static %}
+{% load i18n %}
+
+{% block title %}
+    {{ title | default:"Secret place" }} — {{ block.super }}
+{% endblock %}
+
+{% block content %}
+    <div class="content">
+        <div class="profile-header">{{ title | default:"Secret place" }}</div>
+        <div class="block">
+            <form action="." method="post" enctype="multipart/form-data">
+                {% csrf_token %}
+
+                <div class="form-row">
+                    {{ form.email.label_tag }}
+                    {{ form.email }}
+                    {% if form.email.errors %}<span class="form-row-errors">{{ form.email.errors }}</span>{% endif %}
+                </div>
+
+                {% if features.MEMBERSHIP_ENABLED %}
+                <div class="form-row">
+                    {{ form.days.label_tag }}
+                    {{ form.days }}
+                    {% if form.days.errors %}<span class="form-row-errors">{{ form.days.errors }}</span>{% endif %}
+                </div>
+                {% endif %}
+
+                <button type="submit" class="button">{% translate "Готово" %}</button>
+            </form>
+        </div>
+    </div>
+{% endblock %}
diff --git a/frontend/html/users/admin.html b/frontend/html/users/admin.html
index d5a736a..559e2c9 100644
--- a/frontend/html/users/admin.html
+++ b/frontend/html/users/admin.html
@@ -220,6 +220,7 @@
             </div>
         {% endif %}
 
+        {% if features.MEMBERSHIP_ENABLED %}
         <div class="profile-header">{% translate "Продлить членство" %}</div>
         <div class="block">
             <form action="." method="post" enctype="multipart/form-data">
@@ -233,6 +234,7 @@
                 <button type="submit" class="button">{% translate "Добавить дней" %}</button>
             </form>
         </div>
+        {% endif %}
 
         <div class="profile-header">{% translate "Отправить сообщение" %}</div>
         <div class="block">
diff --git a/landing/forms.py b/landing/forms.py
index b55e124..77ed290 100644
--- a/landing/forms.py
+++ b/landing/forms.py
@@ -1,6 +1,8 @@
 from django import forms
+from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
+from club import features
 from landing.models import GodSettings
 
 
@@ -58,6 +60,7 @@ class GodmodeInviteForm(forms.Form):
 
     days = forms.IntegerField(
         label=_("Дней"),
-        required=True,
-        initial=365,
+        required=features.MEMBERSHIP_ENABLED,
+        disabled=not features.MEMBERSHIP_ENABLED,
+        initial=settings.DEFAULT_MEMBERSHIP_DAYS,
     )
diff --git a/landing/views.py b/landing/views.py
index 808eba1..731a17e 100644
--- a/landing/views.py
+++ b/landing/views.py
@@ -5,11 +5,14 @@ from django.conf import settings
 from django.core.cache import cache
 from django.db.models import Count
 from django.http import Http404
-from django.shortcuts import render, redirect
+from django.shortcuts import redirect, render
+from django.utils.translation import gettext_lazy as _
 
 from auth.helpers import auth_required
+from club import features
 from club.exceptions import AccessDenied
-from landing.forms import GodmodeAboutSettingsEditForm, GodmodeDigestEditForm, GodmodeInviteForm
+from landing.forms import (GodmodeAboutSettingsEditForm, GodmodeDigestEditForm,
+                           GodmodeInviteForm)
 from landing.models import GodSettings
 from notifications.email.invites import send_invited_email
 from users.models.user import User
@@ -96,7 +99,9 @@ def godmode_invite(request):
         form = GodmodeInviteForm(request.POST, request.FILES)
         if form.is_valid():
             email = form.cleaned_data["email"]
-            days = form.cleaned_data["days"]
+            days = 365 * 100  # 100 years
+            if features.MEMBERSHIP_ENABLED:
+                days = form.cleaned_data["days"]
             now = datetime.utcnow()
             user, is_created = User.objects.get_or_create(
                 email=email,
@@ -111,11 +116,11 @@ def godmode_invite(request):
             )
             send_invited_email(request.me, user)
             return render(request, "message.html", {
-                "title": "Эксперт приглашен",
-                "message": f"Сейчас он получит на почту '{email}' уведомление об этом. "
-                           f"Ему будет нужно залогиниться по этой почте и заполнить интро."
+                "title": _("Эксперт приглашен"),
+                "message": _(f"Сейчас он получит на почту '{email}' уведомление об этом. "
+                            f"Ему будет нужно залогиниться по этой почте и заполнить интро.")
             })
     else:
         form = GodmodeInviteForm()
 
-    return render(request, "admin/simple_form.html", {"form": form})
+    return render(request, "admin/invite.html", {"form": form})
diff --git a/users/models/user.py b/users/models/user.py
index 007a9b5..4c27bf0 100644
--- a/users/models/user.py
+++ b/users/models/user.py
@@ -6,6 +6,7 @@ from django.contrib.postgres.fields import ArrayField
 from django.db import models
 from django.db.models import F
 
+from club import features
 from users.models.geo import Geo
 from common.models import ModelDiffMixin
 from utils.slug import generate_unique_slug
@@ -175,7 +176,9 @@ class User(models.Model, ModelDiffMixin):
 
     @property
     def is_active_member(self):
-        return self.is_member and self.membership_expires_at >= datetime.utcnow()
+        if features.MEMBERSHIP_ENABLED:
+            return self.is_member and self.membership_expires_at >= datetime.utcnow()
+        return self.is_member
 
     @property
     def secret_auth_code(self):
-- 
GitLab


From 14d9a086a6db09e7e6be4ccde304bfd5e87f311d Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Tue, 21 Jan 2025 18:43:51 +0700
Subject: [PATCH 4/6] fixed missing translation

---
 locale/en/LC_MESSAGES/django.po | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 25a1929..50a351a 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -2514,10 +2514,8 @@ msgid "Удалить аккаунт и обнулить подписку"
 msgstr "Delete user account and revoke membership"
 
 #: users/forms/admin.py:104
-#, fuzzy
-#| msgid "Добавить дней"
 msgid "Добавить дней членства"
-msgstr "Add days"
+msgstr "Add membership days"
 
 #: users/forms/admin.py:117 users/forms/intro.py:15
 msgid "Никнейм"
-- 
GitLab


From 7d6902c51c74c9e5e53931694059a42f3f80ded8 Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Tue, 21 Jan 2025 12:21:49 +0000
Subject: [PATCH 5/6] added related manage command

---
 club/features.py                              |  1 +
 club/management/__init__.py                   |  0
 club/management/commands/__init__.py          |  0
 .../commands/process_feature_flags.py         | 19 +++++++++++++++++++
 4 files changed, 20 insertions(+)
 create mode 100644 club/management/__init__.py
 create mode 100644 club/management/commands/__init__.py
 create mode 100644 club/management/commands/process_feature_flags.py

diff --git a/club/features.py b/club/features.py
index 14479d6..c53ae52 100644
--- a/club/features.py
+++ b/club/features.py
@@ -10,4 +10,5 @@ PRIVATE_FEED = False
 # Enable/disable timed membership for users
 #  True — users can buy a membership for a certain period of time
 #  False — membership is disabled, all users have access to all features
+# Also see process_membership_flag() in club/management/commands/process_feature_flags.py
 MEMBERSHIP_ENABLED = False
diff --git a/club/management/__init__.py b/club/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/club/management/commands/__init__.py b/club/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/club/management/commands/process_feature_flags.py b/club/management/commands/process_feature_flags.py
new file mode 100644
index 0000000..b6540e9
--- /dev/null
+++ b/club/management/commands/process_feature_flags.py
@@ -0,0 +1,19 @@
+from datetime import datetime
+
+from django.core.management.base import BaseCommand
+
+from club import features
+from users.models.user import User
+
+
+def process_membership_flag():
+    """If membership feature is disabled, extend expiration date for all existing users."""
+    if not features.MEMBERSHIP_ENABLED:
+        User.objects.filter(deleted_at__isnull=True).update(membership_expires_at=datetime(2099, 1, 1))
+
+
+class Command(BaseCommand):
+    help = "Process feature flags"
+
+    def handle(self, *args, **options):
+        process_membership_flag()
-- 
GitLab


From d95764ff01fb935c9ba8d521c4c1c4c2cbc4e354 Mon Sep 17 00:00:00 2001
From: Anton Lebedev <wrawka@gmail.com>
Date: Wed, 22 Jan 2025 07:51:03 +0000
Subject: [PATCH 6/6] added missing migrations

---
 ...ter_historicalpost_type_alter_post_type.py | 23 +++++++++++++
 ...en_tag_name_ru_usertag_name_en_and_more.py | 33 +++++++++++++++++++
 2 files changed, 56 insertions(+)
 create mode 100644 posts/migrations/0006_alter_historicalpost_type_alter_post_type.py
 create mode 100644 users/migrations/0004_tag_name_en_tag_name_ru_usertag_name_en_and_more.py

diff --git a/posts/migrations/0006_alter_historicalpost_type_alter_post_type.py b/posts/migrations/0006_alter_historicalpost_type_alter_post_type.py
new file mode 100644
index 0000000..7d36677
--- /dev/null
+++ b/posts/migrations/0006_alter_historicalpost_type_alter_post_type.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.17 on 2025-01-22 07:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('posts', '0005_auto_20220908_1950'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='historicalpost',
+            name='type',
+            field=models.CharField(choices=[('post', 'Статья'), ('intro', '#intro'), ('link', 'Ссылка'), ('question', 'Вопрос'), ('idea', 'Идея'), ('project', 'Проект'), ('event', 'Событие'), ('weekly_digest', 'Журнал Toolkit'), ('toolkit', 'Тулкит')], db_index=True, default='post', max_length=32),
+        ),
+        migrations.AlterField(
+            model_name='post',
+            name='type',
+            field=models.CharField(choices=[('post', 'Статья'), ('intro', '#intro'), ('link', 'Ссылка'), ('question', 'Вопрос'), ('idea', 'Идея'), ('project', 'Проект'), ('event', 'Событие'), ('weekly_digest', 'Журнал Toolkit'), ('toolkit', 'Тулкит')], db_index=True, default='post', max_length=32),
+        ),
+    ]
diff --git a/users/migrations/0004_tag_name_en_tag_name_ru_usertag_name_en_and_more.py b/users/migrations/0004_tag_name_en_tag_name_ru_usertag_name_en_and_more.py
new file mode 100644
index 0000000..d28c9da
--- /dev/null
+++ b/users/migrations/0004_tag_name_en_tag_name_ru_usertag_name_en_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.17 on 2025-01-22 07:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0003_alter_user_email_digest_type'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tag',
+            name='name_en',
+            field=models.CharField(max_length=64, null=True),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='name_ru',
+            field=models.CharField(max_length=64, null=True),
+        ),
+        migrations.AddField(
+            model_name='usertag',
+            name='name_en',
+            field=models.CharField(max_length=64, null=True),
+        ),
+        migrations.AddField(
+            model_name='usertag',
+            name='name_ru',
+            field=models.CharField(max_length=64, null=True),
+        ),
+    ]
-- 
GitLab