feat: universal rooms (network + chats + topics combined)

This commit is contained in:
vas3k 2023-07-31 17:16:36 +02:00
parent a735761eb0
commit dd10f9fae3
67 changed files with 658 additions and 340 deletions

View File

@ -45,6 +45,7 @@ INSTALLED_APPS = [
"gdpr.apps.GdprConfig",
"badges.apps.BadgesConfig",
"tags.apps.TagsConfig",
"rooms.apps.RoomsConfig",
"misc.apps.MiscConfig",
"simple_history",
"django_q",
@ -82,7 +83,7 @@ TEMPLATES = [
"club.context_processors.data_processor",
"club.context_processors.features_processor",
"authn.context_processors.users.me",
"posts.context_processors.topics.topics",
"posts.context_processors.rooms.rooms",
]
},
}

View File

@ -21,7 +21,8 @@ from comments.views import create_comment, edit_comment, delete_comment, show_co
from common.feature_flags import feature_switch
from landing.views import landing, docs, godmode_network_settings, godmode_digest_settings, godmode_settings, \
godmode_invite
from misc.views import stats, network, robots, generate_ical_invite, generate_google_invite, network_chat
from misc.views import stats, network, robots, generate_ical_invite, generate_google_invite
from rooms.views import redirect_to_room_chat, list_rooms
from notifications.views import render_weekly_digest, email_unsubscribe, email_confirm, render_daily_digest, \
email_digest_switch, link_telegram
from notifications.webhooks import webhook_event
@ -156,8 +157,10 @@ urlpatterns = [
path("search/users.json", api_search_users, name="api_search_users"),
path("search/tags.json", api_search_tags, name="api_search_tags"),
path("room/<slug:topic_slug>/", feed, name="feed_topic"),
path("room/<slug:topic_slug>/<slug:ordering>/", feed, name="feed_topic_ordering"),
path("rooms/", list_rooms, name="list_rooms"),
path("room/<slug:room_slug>/", feed, name="feed_room"),
path("room/<slug:room_slug>/chat/", redirect_to_room_chat, name="redirect_to_room_chat"),
path("room/<slug:room_slug>/<slug:ordering>/", feed, name="feed_room_ordering"),
path("label/<slug:label_code>/", feed, name="feed_label"),
path("label/<slug:label_code>/<slug:ordering>/", feed, name="feed_label_ordering"),
@ -183,7 +186,8 @@ urlpatterns = [
path("docs/<slug:doc_slug>/", docs, name="docs"),
path("network/", network, name="network"),
path("network/chat/<slug:chat_id>/", network_chat, name="network_chat"),
path("network/chat/<slug:chat_id>/", RedirectView.as_view(url="/room/%(chat_id)s/chat/", permanent=True),
name="network_chat"),
# admin features
path("godmode/", godmode_settings, name="godmode_settings"),

View File

@ -23,3 +23,13 @@ class ImageUploadField(forms.ImageField):
convert_to=self.convert_to,
quality=self.quality,
)
class ReverseBooleanField(forms.BooleanField):
def prepare_value(self, value):
return not value
def to_python(self, value):
value = super().to_python(value)
return not value

View File

@ -4,7 +4,7 @@
{% block title %}
{% if post %}
{% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.topic %} [{{ post.topic.name }}]{% endif %} — {{ block.super }}
{% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.room %} [{{ post.room.title }}]{% endif %} — {{ block.super }}
{% endif %}
{% endblock %}

View File

@ -5,66 +5,68 @@
{% load posts %}
{% block title %}
{% if topic %}{{ topic.name }} — {% endif %}{{ block.super }}
{% if room %}{{ room.title }} — {% endif %}{{ block.super }}
{% endblock %}
{% block og_tags %}
<meta property="og:title" content="{% if topic %}{{ topic.name }} — {% endif %}{{ settings.APP_NAME }}">
<meta property="og:title" content="{% if room %}{{ room.title }} — {% endif %}{{ settings.APP_NAME }}">
<meta property="og:site_name" content="{{ settings.APP_NAME }}">
<meta property="og:url" content="{{ settings.APP_HOST }}">
<meta property="og:type" content="website" />
<meta property="og:description" content="{% if topic %}{{ topic.description }}{% endif %}">
<meta property="og:description" content="{% if room %}{{ room.description }}{% endif %}">
<meta property="og:image" content="{% static "images/share.png" %}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{% if topic %}{{ topic.name }} — {% endif %}{{ settings.APP_NAME }}">
<meta name="twitter:description" content="{% if topic %}{{ topic.description }}{% endif %}">
<meta name="twitter:title" content="{% if room %}{{ room.title }} — {% endif %}{{ settings.APP_NAME }}">
<meta name="twitter:description" content="{% if room %}{{ room.subtitle }}{% endif %}">
<meta name="twitter:image" content="{% static "images/share.png" %}">
{% endblock %}
{% block feed_content %}
{% if topic %}
<div class="feed-topic-header" style="background-color: {{ topic.color }};">
<span class="topic-icon feed-topic-header-icon"><img src="{{ topic.icon }}" alt="Иконка комнаты {{ topic.name }}" loading="lazy" /></span>
<h1 class="feed-topic-header-name">{{ topic.name }}</h1>
{% if room %}
<div class="feed-topic-header" style="background-color: {{ room.color }};">
<div class="feed-topic-header-title">
<span class="room-icon feed-topic-header-icon"><img src="{{ room.image }}" alt="Иконка комнаты {{ room.title }}" loading="lazy" /></span>
<a href="{% url "feed_room" room.slug %}" class="feed-topic-header-name">{{ room.title }}</a>
</div>
<span class="feed-topic-header-desctiption">
{{ topic.description | markdown }}
{{ room.description | markdown }}
</span>
{% if topic.chat_url and topic.chat_name %}
{% if room.chat_url and room.chat_name %}
<span class="feed-topic-header-footer">
<i class="fab fa-telegram-plane"></i>&nbsp;<strong>Официальный чат:</strong> <a href="{{ topic.chat_url }}" rel="noreferrer" target="_blank">{{ topic.chat_name }}</a>
<i class="fab fa-telegram-plane"></i>&nbsp;<strong>Официальный чат:</strong> <a href="{{ room.chat_url }}" rel="noreferrer" target="_blank">{{ room.chat_name }}</a>
</span>
{% endif %}
</div>
{% endif %}
<div class="feed-ordering">
<a href="{% feed_ordering_url topic label_code post_type "activity" %}"
<a href="{% feed_ordering_url room label_code post_type "activity" %}"
class="feed-ordering-item {% if ordering == "activity" %}feed-ordering-item-is-active{% endif %}"
>
Фид
</a>
<a href="{% feed_ordering_url topic label_code post_type "new" %}"
<a href="{% feed_ordering_url room label_code post_type "new" %}"
class="feed-ordering-item {% if ordering == "new" %}feed-ordering-item-is-active{% endif %}"
>
Новое
</a>
<a href="{% feed_ordering_url topic label_code post_type "hot" %}"
<a href="{% feed_ordering_url room label_code post_type "hot" %}"
class="feed-ordering-item {% if ordering == "hot" %}feed-ordering-item-is-active{% endif %}"
>
Обсуждаемое
</a>
{% if me and me.created_at < date_month_ago %}
<a href="{% feed_ordering_url topic label_code post_type "top_month" %}"
<a href="{% feed_ordering_url room label_code post_type "top_month" %}"
class="feed-ordering-item {% if ordering == "top" or ordering == "top_week" or ordering == "top_month" %}feed-ordering-item-is-active{% endif %}"
>
Лучшее
</a>
{% else %}
<a href="{% feed_ordering_url topic label_code post_type "top" %}"
<a href="{% feed_ordering_url room label_code post_type "top" %}"
class="feed-ordering-item {% if ordering == "top" or ordering == "top_week" or ordering == "top_month" %}feed-ordering-item-is-active{% endif %}"
>
Лучшее

View File

@ -43,24 +43,24 @@
<div class="block network-block" id="{{ group.code }}">
<a href="#{{ group.code }}" class="network-header">{{ group.title }}</a>
{% if group.items %}
{% if group.rooms %}
<div class="network-channels">
{% for item in group.items.all %}
<a href="{{ item.get_private_url }}" target="_blank" class="network-channel">
{% if item.image %}
<span class="avatar network-channel-icon"><img src="{{ item.image }}" alt="{{ item.name }}"></span>
{% for room in group.rooms.all %}
<a href="{{ room.get_private_url }}" target="_blank" class="network-channel">
{% if room.image %}
<span class="avatar network-channel-icon"><img src="{{ room.image }}" alt="{{ room.title }}"></span>
{% endif %}
{% if item.icon %}
<span class="network-channel-badge">{{ item.icon | safe }}</span>
{% if room.icon %}
<span class="network-channel-badge">{{ room.icon | safe }}</span>
{% endif %}
{% if item.name %}
<span class="network-channel-name">{{ item.name | safe }}</span>
{% if room.title %}
<span class="network-channel-name">{{ room.title | safe }}</span>
{% endif %}
{% if item.description %}
<span class="network-channel-description">{{ item.description | safe }}</span>
{% if room.subtitle %}
<span class="network-channel-description">{{ room.subtitle | safe }}</span>
{% endif %}
</a>
{% endfor %}

View File

@ -39,11 +39,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row">
<label for="{{ form.side_a.id_for_label }}" class="form-label">Стороны батла</label>
<div class="form-row-center">

View File

@ -1,24 +1,80 @@
<details class="block compose-form-advanced" {% if form.instance.coauthors or form.instance.collectible_tag_code %}open{% endif %}>
<summary class="compose-form-advanced-summary">Продвинутые настройки</summary>
<details class="block compose-form-advanced" {% if form.instance.room %}open{% endif %}>
<summary class="compose-form-advanced-summary">🚪 Запостить в комнату</summary>
<div class="clearfix50"></div>
{% if post_type == "post" or post_type == "guide" or post_type == "project" or post_type == "event" %}
<div class="block-description-center">
Бот Клуба автоматически унесёт ваш пост в чат выбранной вами комнаты.
Возможно, там он быстрее найдет заинтересованных темой.
</div>
<div class="clearfix50"></div>
<div class="form-row compose-form-room">
<simple-select
id="{{ form.room.html_name }}"
initial-value="{{ form.room.value|default_if_none:'' }}"
:options="[
{% for room in rooms %}{label: '{{ room.emoji }} {{ room.title }}', code: '{{ room.slug }}'},{% endfor %}
]"
>
</simple-select>
{% if form.room.errors %}
<span class="form-row-errors">{{ form.room.errors }}</span>
{% endif %}
</div>
<div class="form-row form-row-checkbox">
{{ form.is_visible_in_feeds }}
<label for="{{ form.is_visible_in_feeds.id_for_label }}">{{ form.is_visible_in_feeds.label }}</label>
{% if form.is_visible_in_feeds.errors %}
<span class="form-row-errors">{{ form.is_visible_in_feeds.errors }}</span>
{% endif %}
</div>
</details>
{% if post_type == "post" or post_type == "guide" or post_type == "project" or post_type == "event" %}
<details class="block compose-form-advanced" {% if form.instance.coauthors %}open{% endif %}>
<summary class="compose-form-advanced-summary">👨‍👨‍👧‍👧 Добавить соавторов к посту</summary>
<div class="clearfix50"></div>
<div class="block-description-center">
Соавторы могут редактировать пост и отображаются в списке авторов
</div>
<div class="clearfix50"></div>
<div class="form-row compose-form-coauthors">
<label for="{{ form.coauthors.id_for_label }}" class="form-label">{{ form.coauthors.label }}</label>
{{ form.coauthors }}
{% if form.coauthors.errors %}
<span class="form-row-errors">{{ form.coauthors.errors }}</span>
{% endif %}
<span class="form-row-help form-row-help-wide">
Список никнеймов через запятую. Они смогут тоже редактировать пост, но лучше не делать этого одновременно — изменения одного автора могут затереть другого.
Список никнеймов через запятую
</span>
</div>
{% endif %}
</details>
{% endif %}
<details class="block compose-form-advanced" {% if form.instance.collectible_tag_code %}open{% endif %}>
<summary class="compose-form-advanced-summary">🏷️ Прикрепить коллекционный тег</summary>
<div class="clearfix50"></div>
<div class="block-description-center">
Пользователи могут подписываться на коллекционные теги и автоматически получать уведомления, когда кто-то пишет новый пост с интересным им тегом.
</div>
<div class="clearfix50"></div>
<div class="form-row compose-form-collectible-tag">
<label for="{{ form.collectible_tag_code.id_for_label }}" class="form-label">{{ form.collectible_tag_code.label }}</label>
<multi-select
<tag-select
initial-value="{{ form.collectible_tag_code.value|default_if_none:'' }}"
id="{{ form.collectible_tag_code.html_name }}"
search-url="/search/tags.json?prefix="
@ -28,43 +84,43 @@
label-invalid-input="Каждый тег обязан начинаться с emoji, потом идёт пробел и название."
label-valid-input="Вы добавите этот тег первым!"
>
</multi-select>
{% if form.collectible_tag_code.errors %}
<span class="form-row-errors">{{ form.collectible_tag_code.errors }}</span>
{% endif %}
</tag-select>
<span class="form-row-help form-row-help-wide">
Каждый тег обязан начинаться с <a href="https://emojipedia.org/" target="_blank">emoji</a>, потом идёт пробел и название.
</span>
{% if form.collectible_tag_code.errors %}
<span class="form-row-errors">{{ form.collectible_tag_code.errors }}</span>
{% endif %}
</div>
</details>
{% if mode == "create" or form.instance.is_public or form.instance.comment_count < 10 or form.instance.published_at is None or me.is_moderator %}
<div class="big-radio compose-visibility">
<div class="big-radio-item">
{{ form.is_public.0.tag }}
<label for="{{ form.is_public.0.id_for_label }}" class="big-radio-label">
<i class="fas fa-globe-americas"></i>
<span class="big-radio-title">{{ form.is_public.0.choice_label }}</span>
<span class="big-radio-description">
Пост виден снаружи, его можно пошарить в соцсеточки.
Такие посты развивают Клуб и чаще попадают в дайджесты.
</span>
</label>
</div>
<div class="big-radio compose-visibility">
<div class="big-radio-item">
{{ form.is_public.0.tag }}
<label for="{{ form.is_public.0.id_for_label }}" class="big-radio-label">
<i class="fas fa-globe-americas"></i>
<span class="big-radio-title">{{ form.is_public.0.choice_label }}</span>
<span class="big-radio-description">
Пост виден снаружи, его можно пошарить в соцсеточки.
Такие посты развивают Клуб и чаще попадают в дайджесты.
</span>
</label>
</div>
<div class="big-radio-item">
{{ form.is_public.1.tag }}
<label for="{{ form.is_public.1.id_for_label }}" class="big-radio-label">
<i class="fas fa-lock"></i>
<span class="big-radio-title">{{ form.is_public.1.choice_label }}</span>
<span class="big-radio-description">
Пост для членов Клуба.
Для обсуждения чувствительных тем и организации внутренних движух.
Сменить тип потом нельзя.
</span>
</label>
<div class="big-radio-item">
{{ form.is_public.1.tag }}
<label for="{{ form.is_public.1.id_for_label }}" class="big-radio-label">
<i class="fas fa-lock"></i>
<span class="big-radio-title">{{ form.is_public.1.choice_label }}</span>
<span class="big-radio-description">
Пост для членов Клуба.
Для обсуждения чувствительных тем и организации внутренних движух.
Сменить тип потом нельзя.
</span>
</label>
</div>
</div>
</div>
{% endif %}

View File

@ -26,11 +26,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.title }}
{% if form.title.errors %}<span class="form-row-errors">{{ form.title.errors }}</span>{% endif %}

View File

@ -26,11 +26,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.title }}
{% if form.title.errors %}<span class="form-row-errors">{{ form.title.errors }}</span>{% endif %}

View File

@ -40,11 +40,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.url }}
{% if form.url.errors %}<span class="form-row-errors">{{ form.url.errors }}</span>{% endif %}

View File

@ -40,11 +40,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.title }}
{% if form.title.errors %}<span class="form-row-errors">{{ form.title.errors }}</span>{% endif %}

View File

@ -48,11 +48,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.title }}
{% if form.title.errors %}<span class="form-row-errors">{{ form.title.errors }}</span>{% endif %}

View File

@ -30,11 +30,6 @@
<form action="." method="post" class="compose-form" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row form-row-center">
{{ form.topic }}
{% if form.topic.errors %}<span class="form-row-errors">{{ form.topic.errors }}</span>{% endif %}
</div>
<div class="form-row compose-form-title">
{{ form.title }}
{% if form.title.errors %}<span class="form-row-errors">{{ form.title.errors }}</span>{% endif %}

View File

@ -32,9 +32,9 @@
</post-upvote>
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -46,9 +46,9 @@
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -34,9 +34,9 @@
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -32,9 +32,9 @@
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -31,9 +31,9 @@
</post-upvote>
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -36,9 +36,9 @@
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -34,9 +34,9 @@
</div>
<div class="feed-post-footer">
{% if post.topic %}
<span class="feed-post-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
<span class="feed-post-room">
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
</span>
{% endif %}

View File

@ -6,9 +6,9 @@
{% block post %}
<div class="battle-header">
{% if post.topic %}
{% if post.room %}
<div class="battle-header-topic">
{% include "posts/widgets/topic.html" with topic=post.topic type="post" %}
{% include "rooms/widgets/room.html" with room=post.room type="post" %}
</div>
{% endif %}
@ -109,7 +109,7 @@
</a>
</div>
{% endif %}
{% if comments %}
<div class="post-comments-list">
{% include "comments/list.html" with comments=comments reply_form=reply_form type="battle" %}

View File

@ -7,8 +7,8 @@
{% block post_header %}
<header class="post-header">
<div class="post-info">
{% if post.topic %}
{% include "posts/widgets/topic.html" with topic=post.topic type="post" %}
{% if post.room %}
{% include "rooms/widgets/room.html" with room=post.room type="post" %}
{% endif %}
</div>
<h1 class="post-title">

View File

@ -4,7 +4,7 @@
{% load text_filters %}
{% block title %}
{% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.topic %} [{{ post.topic.name }}]{% endif %} — {{ block.super }}
{% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.room %} [{{ post.room.title }}]{% endif %} — {{ block.super }}
{% endblock %}
{% block og_tags %}

View File

@ -9,8 +9,8 @@
{% block post_header %}
<header class="post-header">
<div class="post-info">
{% if post.topic %}
{% include "posts/widgets/topic.html" with topic=post.topic type="post" %}
{% if post.room %}
{% include "rooms/widgets/room.html" with room=post.room type="post" %}
{% endif %}
</div>

View File

@ -54,8 +54,8 @@
</div>
<div class="post-info">
{% if post.topic %}
{% include "posts/widgets/topic.html" with topic=post.topic type="inline" %}
{% if post.room %}
{% include "rooms/widgets/room.html" with room=post.room type="inline" %}
{% endif %}
<div class="post-actions-line">
{% include "posts/widgets/post_actions_line.html" %}

View File

@ -1,9 +0,0 @@
<a href="{% url "feed_topic" topic.slug %}" class="topic {% if type %}topic-type-{{type}}{% endif %}" style="{% if topic.style %}{{ topic.style }}{% else %}background-color: {{ topic.color }};{% endif %}">
<span class="topic-gradient"></span>
<span class="topic-icon-wrapper">
<span class="topic-icon"><img src="{{ topic.icon }}" alt="Иконка комнаты {{ topic.name }}" loading="lazy" /></span>
</span>
<span class="topic-name-wrapper">
<span class="topic-name">{{ topic.name }}</span>
</span>
</a>

View File

@ -0,0 +1,40 @@
{% extends "sidebar_layout.html" %}
{% load text_filters %}
{% load static %}
{% load paginator %}
{% load posts %}
{% block title %}
Комнаты — {{ block.super }}
{% endblock %}
{% block og_tags %}
<meta property="og:title" content="Комнаты — {{ settings.APP_NAME }}">
<meta property="og:site_name" content="{{ settings.APP_NAME }}">
<meta property="og:url" content="{{ settings.APP_HOST }}">
<meta property="og:type" content="website" />
<meta property="og:image" content="{% static "images/share.png" %}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Комнаты — {{ settings.APP_NAME }}">
<meta name="twitter:image" content="{% static "images/share.png" %}">
{% endblock %}
{% block feed_content %}
{% for room in rooms %}
<div class="feed-topic-header" style="background-color: {{ room.color }};">
<div class="feed-topic-header-title">
<span class="room-icon feed-topic-header-icon"><img src="{{ room.image }}" alt="Иконка комнаты {{ room.title }}" loading="lazy" /></span>
<a href="{% url "feed_room" room.slug %}" class="feed-topic-header-name">{{ room.title }}</a>
</div>
<span class="feed-topic-header-desctiption">
{{ room.description | markdown }}
</span>
{% if room.chat_url and room.chat_name %}
<span class="feed-topic-header-footer">
<i class="fab fa-telegram-plane"></i>&nbsp;<strong>Официальный чат:</strong> <a href="{{ room.chat_url }}" rel="noreferrer" target="_blank">{{ room.chat_name }}</a>
</span>
{% endif %}
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,9 @@
<a href="{% url "feed_room" room.slug %}" class="topic {% if type %}topic-type-{{type}}{% endif %}" style="{% if room.style %}{{ room.style }}{% else %}background-color: {{ room.color }};{% endif %}">
<span class="topic-gradient"></span>
<span class="topic-icon-wrapper">
<span class="topic-icon"><img src="{{ room.image }}" alt="Иконка комнаты {{ room.title }}" loading="lazy" /></span>
</span>
<span class="topic-name-wrapper">
<span class="topic-name">{{ room.title }}</span>
</span>
</a>

View File

@ -20,7 +20,7 @@
</li>
<li>
<a href="{% url "network" %}">
💬 Сеть
💬 Чаты
</a>
</li>
<li>
@ -82,10 +82,13 @@
<div class="feed-sidebar-block">
<div class="feed-sidebar-header">Комнаты</div>
<div class="feed-sidebar-topics">
{% for topic in topics %}
{% include "posts/widgets/topic.html" with topic=topic type="card" %}
{% for room in rooms|slice:":10" %}
{% include "rooms/widgets/room.html" with room=room type="card" %}
{% endfor %}
</div>
<div class="feed-sidebar-topics-more">
<a class="button button-small button-inverted" href="{% url "list_rooms" %}">Все комнаты &rarr;</a>
</div>
</div>
</aside>
<div class="feed-main">

View File

@ -15,7 +15,7 @@
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
vertical-align: middle;
}
form .form-row p {

View File

@ -466,7 +466,7 @@ input[type=range]:focus::-ms-fill-upper {
width: auto;
transform: scale(1.5);
position: relative;
top: -4px;
top: -2px;
}
.form-row-help {

View File

@ -154,3 +154,11 @@
font-size: 130%;
text-align: center;
}
.compose-form-room select {
width: 100%;
padding: 10px;
font-size: 110%;
font-weight: 500;
appearance: auto;
}

View File

@ -99,6 +99,12 @@
max-width: 100%;
}
.feed-sidebar-topics-more {
text-align: center;
padding: 20px 5px;
font-weight: 500;
}
.feed-main {
}
@ -120,20 +126,27 @@
color: #FFF;
}
.feed-topic-header-name {
margin: 0;
padding-top: 10px;
padding-bottom: 20px;
}
.feed-topic-header-title {
display: flex;
flex-direction: row;
gap: 20px;
align-items: center;
}
.feed-topic-header-icon {
width: 64px;
height: 64px;
border-width: 4px;
float: left;
margin-right: 15px;
position: relative;
}
.feed-topic-header-name {
font-size: 210%;
font-weight: 700;
text-decoration: none;
}
.feed-topic-header-icon {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
position: relative;
border: solid 2px var(--bg-color);
}
.feed-topic-header-desctiption {
display: block;
@ -326,7 +339,7 @@
height: 100%;
}
.feed-post-topic {
.feed-post-room {
padding-right: 10px;
display: inline-block;
}

View File

@ -0,0 +1,42 @@
<template>
<div class="input-select">
<input type="hidden" v-model="currentValue.code" :name="id" />
<v-select
:options="options"
:value="currentValue"
@input="onChange"
>
</v-select>
</div>
</template>
<script>
export default {
name: "SimpleSelect",
props: {
id: {
type: String,
required: true,
},
initialValue: {
type: String,
required: false,
},
options: {
type: Array,
required: true,
}
},
data() {
return {
currentValue: this.options.find(x => x.code === this.initialValue) || {},
};
},
methods: {
onChange(newValue) {
this.currentValue = newValue;
}
}
};
</script>

View File

@ -26,7 +26,8 @@ Vue.component("friend-button", () => import("./components/FriendButton.vue"));
Vue.component("comment-scroll-arrow", () => import("./components/CommentScrollArrow.vue"));
Vue.component("comment-markdown-editor", () => import("./components/CommentMarkdownEditor.vue"));
Vue.component("v-select", vSelect);
Vue.component("multi-select", () => import("./components/MultiSelect.vue"));
Vue.component("tag-select", () => import("./components/TagSelect.vue"));
Vue.component("simple-select", () => import("./components/SimpleSelect.vue"));
// Since our pages have user-generated content, any fool can insert "{{" on the page and break it.
// We have no other choice but to completely turn off template matching and leave it on only for components.

View File

@ -24,7 +24,7 @@ def post_to_json(post: Post) -> dict:
"slug": post.slug,
"author_id": str(post.author_id),
"type": post.type,
"topic": post.topic.name if post.topic else None,
"room": post.room.title if post.room else None,
"label": post.label,
"title": post.title,
"text": post.text,

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from misc.models import ProTip, NetworkGroup, NetworkItem
from misc.models import ProTip, NetworkGroup
class ProTipsAdmin(admin.ModelAdmin):
@ -31,18 +31,3 @@ class NetworkGroupAdmin(admin.ModelAdmin):
admin.site.register(NetworkGroup, NetworkGroupAdmin)
class NetworkItemAdmin(admin.ModelAdmin):
list_display = (
"name",
"description",
"group",
"url",
"index",
)
ordering = ("index",)
search_fields = ["title", "description"]
admin.site.register(NetworkItem, NetworkItemAdmin)

View File

@ -0,0 +1,16 @@
# Generated by Django 3.2.13 on 2023-07-31 14:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('misc', '0003_alter_networkitem_id'),
]
operations = [
migrations.DeleteModel(
name='NetworkItem',
),
]

View File

@ -2,7 +2,6 @@ from datetime import datetime
from uuid import uuid4
from django.db import models
from django.urls import reverse
class ProTip(models.Model):
@ -46,28 +45,3 @@ class NetworkGroup(models.Model):
@classmethod
def visible_objects(cls):
return cls.objects.filter(is_visible=True)
class NetworkItem(models.Model):
id = models.CharField(primary_key=True, max_length=32, default=uuid4)
group = models.ForeignKey(NetworkGroup, related_name="items", db_index=True, null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=128, null=True, blank=True)
description = models.CharField(max_length=256, null=True, blank=True)
image = models.URLField(null=False)
icon = models.CharField(max_length=256, null=True, blank=True)
url = models.URLField(null=False)
telegram_chat_id = models.CharField(max_length=32, null=True, blank=True)
index = models.PositiveIntegerField(default=0)
class Meta:
db_table = "network_items"
ordering = ["index"]
def get_private_url(self):
if self.url:
return reverse("network_chat", kwargs={"chat_id": self.id})
return None

View File

@ -3,15 +3,14 @@ from urllib.parse import urlencode
import pytz
from django.db.models import Count, Q, Sum
from django.http import HttpResponse, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.views.decorators.http import require_GET
from icalendar import Calendar, Event
from authn.decorators.auth import require_auth
from badges.models import UserBadge
from landing.models import GodSettings
from misc.models import NetworkGroup, NetworkItem
from misc.models import NetworkGroup
from users.models.achievements import Achievement
from users.models.user import User
@ -67,14 +66,6 @@ def network(request):
})
@require_auth
def network_chat(request, chat_id):
network_item = get_object_or_404(NetworkItem, id=chat_id)
if not network_item.url:
raise Http404()
return redirect(network_item.url, permanent=False)
@require_GET
def robots(request):
lines = [

View File

@ -40,18 +40,19 @@ def announce_in_club_chats(post):
])
# announce to public chat
send_telegram_message(
chat=CLUB_CHAT,
text=render_html_message("channel_post_announce.html", post=post),
parse_mode=telegram.ParseMode.HTML,
disable_preview=True,
reply_markup=post_reply_markup,
)
if post.topic and post.topic.chat_id:
# announce to the topic chat
if post.is_visible_in_feeds or not post.room:
send_telegram_message(
chat=Chat(id=post.topic.chat_id),
chat=CLUB_CHAT,
text=render_html_message("channel_post_announce.html", post=post),
parse_mode=telegram.ParseMode.HTML,
disable_preview=True,
reply_markup=post_reply_markup,
)
if post.room and post.room.chat_id:
# announce to the room chat
send_telegram_message(
chat=Chat(id=post.room.chat_id),
text=render_html_message("channel_post_announce.html", post=post),
parse_mode=telegram.ParseMode.HTML,
disable_preview=True,

View File

@ -1,4 +1,4 @@
{% if post.emoji %}{{ post.emoji }} {% endif %}<b>{% if post.prefix and post.prefix != post.emoji %}{{ post.prefix }} {% endif %}<a href="{{ settings.APP_HOST }}{% url "show_post" post.type post.slug %}">{{ post.title }}</a> {% if post.topic %} [{{ post.topic.name }}]{% endif %}</b>
{% if post.emoji %}{{ post.emoji }} {% endif %}<b>{% if post.prefix and post.prefix != post.emoji %}{{ post.prefix }} {% endif %}<a href="{{ settings.APP_HOST }}{% url "show_post" post.type post.slug %}">{{ post.title }}</a> {% if post.room %} [{{ post.room.title }}]{% endif %}</b>
{% load posts %}{% render_plain post 350 %}

View File

@ -1,4 +1,4 @@
Ваш чувак {{ post.author.full_name }} написал пост 👇
{% if post.emoji %}{{ post.emoji }} {% endif %}<b>{% if post.prefix %}{{ post.prefix }} {% endif %}<a href="{{ settings.APP_HOST }}{% url "show_post" post.type post.slug %}">{{ post.title }}</a> {% if post.topic %} [{{ post.topic.name }}]{% endif %}</b>
{% if post.emoji %}{{ post.emoji }} {% endif %}<b>{% if post.prefix %}{{ post.prefix }} {% endif %}<a href="{{ settings.APP_HOST }}{% url "show_post" post.type post.slug %}">{{ post.title }}</a> {% if post.room %} [{{ post.room.title }}]{% endif %}</b>
{% load posts %}{% render_plain post 350 %}

View File

@ -1,7 +1,7 @@
🔥 <b>NEW: {% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.topic %} [{{ post.topic.name }}]{% endif %}</b>
🔥 <b>NEW: {% if post.prefix %}{{ post.prefix }} {% endif %}{{ post.title }}{% if post.room %} [{{ post.room.title }}]{% endif %}</b>
<b>Автор:</b> {{ post.author.slug }} {% if post.author.telegram_data %}(tg: @{{ post.author.telegram_data.username }}){% endif %}
{% if post.url %}<b>Ссылка:</b> {{ post.url }}{% endif %}{% if post.collectible_tag_code %}🚨 <b>Прикреплён коллекционный тег:</b> {{ post.collectible_tag_code }}{% endif %}
{% if post.url %}<b>Ссылка:</b> {{ post.url }}{% endif %}{% if post.collectible_tag_code %}🚨 <b>Прикреплён коллекционный тег:</b> {{ post.collectible_tag_code }}{% endif %}{% if not post.is_visible_in_feeds %}👀 <b>Пост виден только в комнате! Можно модерить без жести.</b>{% endif %}
{% load posts %}{% render_plain post 350 %}

View File

@ -2,7 +2,6 @@ from django.contrib import admin
from posts.models.linked import LinkedPost
from posts.models.post import Post
from posts.models.topics import Topic
class PostsAdmin(admin.ModelAdmin):
@ -26,25 +25,6 @@ class PostsAdmin(admin.ModelAdmin):
admin.site.register(Post, PostsAdmin)
class TopicsAdmin(admin.ModelAdmin):
list_display = (
"slug",
"name",
"icon",
"color",
"style",
"chat_name",
"last_activity_at",
"is_visible",
"is_visible_in_feeds",
)
ordering = ("slug",)
search_fields = ["slug", "name"]
admin.site.register(Topic, TopicsAdmin)
class LinkedPostsAdmin(admin.ModelAdmin):
list_display = (
"post_from",

View File

@ -0,0 +1,9 @@
from rooms.models import Room
def rooms(request):
rooms = Room.objects.filter(is_visible=True, is_open_for_posting=True).order_by("-last_activity_at").all()
return {
"rooms": rooms,
"rooms_map": {room.slug: room for room in rooms},
}

View File

@ -1,9 +0,0 @@
from posts.models.topics import Topic
def topics(request):
topics = Topic.objects.filter(is_visible=True).all()
return {
"topics": Topic.objects.filter(is_visible=True).all(),
"topics_map": {t.slug: t for t in topics},
}

View File

@ -58,6 +58,11 @@ class PostCuratorForm(forms.Form):
required=False,
)
show_in_feeds = forms.BooleanField(
label="Вернуть на главную (или вытащить из комнаты)",
required=False,
)
re_ping_collectible_tag_owners = forms.BooleanField(
label="Перепингануть подписчиков коллективного тега",
required=False,

View File

@ -9,8 +9,8 @@ from slugify import slugify_filename
from common.regexp import EMOJI_RE
from common.url_metadata_parser import parse_url_preview
from posts.models.post import Post
from posts.models.topics import Topic
from common.forms import ImageUploadField
from common.forms import ImageUploadField, ReverseBooleanField
from rooms.models import Room
from tags.models import Tag
from users.models.user import User
@ -58,17 +58,22 @@ class CollectibleTagField(forms.CharField):
class PostForm(forms.ModelForm):
topic = forms.ModelChoiceField(
room = forms.ModelChoiceField(
label="Комната",
required=False,
empty_label="Для всех",
queryset=Topic.objects.filter(is_visible=True).all(),
queryset=Room.objects.filter(is_visible=True, is_open_for_posting=True).order_by("title").all(),
)
collectible_tag_code = CollectibleTagField(
label="Прикрепить коллекционный тег",
max_length=32,
required=False,
)
is_visible_in_feeds = ReverseBooleanField(
label="Пост только для этой комнаты (не отображается на главной)",
initial=True,
required=False
)
is_public = forms.ChoiceField(
label="Виден ли в большой интернет?",
choices=((True, "Публичный пост"), (False, "Только для своих")),
@ -79,15 +84,6 @@ class PostForm(forms.ModelForm):
class Meta:
abstract = True
def clean_topic(self):
topic = self.cleaned_data["topic"]
if topic and not topic.is_visible_in_feeds:
# topic settings are more important
self.instance.is_visible_in_feeds = False
return topic
def clean_coauthors(self):
coauthors = [coauthor.replace("@", "", 1) for coauthor in self.cleaned_data.get("coauthors")]
if not coauthors:
@ -104,6 +100,17 @@ class PostForm(forms.ModelForm):
return coauthors
def clean_is_visible_in_feeds(self):
new_value = self.cleaned_data.get("is_visible_in_feeds")
if new_value is None:
return self.instance.is_visible_in_feeds
if new_value and not self.instance.is_visible_in_feeds:
raise ValidationError("Нельзя вытаскивать посты обратно из комнат. Только модератор может это сделать")
return new_value
class PostTextForm(PostForm):
title = forms.CharField(
@ -136,9 +143,10 @@ class PostTextForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"coauthors",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -179,8 +187,9 @@ class PostLinkForm(PostForm):
"title",
"text",
"url",
"topic",
"room",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -227,8 +236,9 @@ class PostQuestionForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public"
]
@ -259,8 +269,9 @@ class PostIdeaForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -370,9 +381,10 @@ class PostEventForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"coauthors",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public"
]
@ -478,11 +490,12 @@ class PostProjectForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"url",
"image",
"coauthors",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -528,8 +541,9 @@ class PostBattleForm(PostForm):
model = Post
fields = [
"text",
"topic",
"room",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -641,9 +655,10 @@ class PostGuideForm(PostForm):
fields = [
"title",
"text",
"topic",
"room",
"coauthors",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]
@ -692,9 +707,10 @@ class PostThreadForm(PostForm):
"title",
"text",
"comment_template",
"topic",
"room",
"coauthors",
"collectible_tag_code",
"is_visible_in_feeds",
"is_public",
]

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.13 on 2023-07-31 10:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('rooms', '0001_initial'),
('posts', '0026_auto_20220615_1047'),
]
operations = [
migrations.AddField(
model_name='post',
name='room',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='rooms.room'),
),
migrations.RunSQL(
sql="update posts set room_id = topic_id where topic_id is not null",
reverse_sql="update posts set topic_id = room_id where room_id is not null"
),
migrations.RemoveField(
model_name='post',
name='topic',
),
migrations.DeleteModel(
name='Topic',
),
]

View File

@ -12,7 +12,7 @@ from simple_history.models import HistoricalRecords
from common.data.labels import LABELS
from common.models import ModelDiffMixin
from posts.models.topics import Topic
from rooms.models import Room
from users.models.user import User
from utils.slug import generate_unique_slug
@ -74,7 +74,7 @@ class Post(models.Model, ModelDiffMixin):
author = models.ForeignKey(User, related_name="posts", db_index=True, on_delete=models.CASCADE)
type = models.CharField(max_length=32, choices=TYPES, default=TYPE_POST, db_index=True)
topic = models.ForeignKey(Topic, related_name="posts", null=True, db_index=True, on_delete=models.SET_NULL)
room = models.ForeignKey(Room, related_name="posts", null=True, db_index=True, on_delete=models.SET_NULL)
label_code = models.CharField(max_length=16, null=True, db_index=True)
collectible_tag_code = models.CharField(max_length=32, null=True)
coauthors = ArrayField(models.CharField(max_length=32), default=list, null=False, db_index=True)
@ -126,7 +126,7 @@ class Post(models.Model, ModelDiffMixin):
"is_visible_in_feeds",
"is_pinned_until",
"is_shadow_banned",
"topic",
"room",
],
)
@ -274,7 +274,7 @@ class Post(models.Model, ModelDiffMixin):
@classmethod
def visible_objects(cls):
return cls.objects.filter(is_visible=True).select_related("topic", "author")
return cls.objects.filter(is_visible=True).select_related("room", "author")
@classmethod
def objects_for_user(cls, user):

View File

@ -1,34 +0,0 @@
from datetime import datetime, timedelta
from django.db import models
class Topic(models.Model):
slug = models.CharField(primary_key=True, max_length=32, unique=True)
name = models.CharField(max_length=64, null=False)
icon = models.URLField(null=True)
description = models.TextField(null=True)
color = models.CharField(max_length=16, null=False)
style = models.CharField(max_length=256, default="", null=True)
chat_name = models.CharField(max_length=128, null=True)
chat_url = models.URLField(null=True)
chat_id = models.CharField(max_length=64, null=True)
last_activity_at = models.DateTimeField(auto_now_add=True, null=False)
is_visible = models.BooleanField(default=True)
is_visible_in_feeds = models.BooleanField(default=True)
class Meta:
db_table = "topics"
ordering = ["-last_activity_at"]
def __str__(self):
return self.name
def update_last_activity(self):
now = datetime.utcnow()
if self.last_activity_at < now - timedelta(minutes=5):
return Topic.objects.filter(slug=self.slug).update(last_activity_at=now)

View File

@ -56,9 +56,9 @@ def render_plain(context, post, truncate=None):
@register.simple_tag()
def feed_ordering_url(topic, label_code, post_type, ordering_type):
if topic:
return reverse("feed_topic_ordering", args=[topic.slug, ordering_type])
def feed_ordering_url(room, label_code, post_type, ordering_type):
if room:
return reverse("feed_room_ordering", args=[room.slug, ordering_type])
elif label_code:
return reverse("feed_label_ordering", args=[label_code, ordering_type])
else:

View File

@ -166,6 +166,11 @@ def do_common_admin_and_curator_actions(request, post, data):
post.is_visible_in_feeds = False
post.save()
# Show back in feeds
if data["show_in_feeds"]:
post.is_visible_in_feeds = True
post.save()
# Ping collectible tag owners again
if data["re_ping_collectible_tag_owners"]:
if post.collectible_tag_code:

View File

@ -9,12 +9,12 @@ from common.feature_flags import feature_switch, noop
from common.pagination import paginate
from posts.helpers import POST_TYPE_ALL, ORDERING_ACTIVITY, ORDERING_NEW, sort_feed
from posts.models.post import Post
from posts.models.topics import Topic
from rooms.models import Room
from users.models.mute import Muted
@feature_switch(features.PRIVATE_FEED, yes=require_auth, no=noop)
def feed(request, post_type=POST_TYPE_ALL, topic_slug=None, label_code=None, ordering=ORDERING_ACTIVITY):
def feed(request, post_type=POST_TYPE_ALL, room_slug=None, label_code=None, ordering=ORDERING_ACTIVITY):
post_type = post_type or Post
if request.me:
@ -27,12 +27,12 @@ def feed(request, post_type=POST_TYPE_ALL, topic_slug=None, label_code=None, ord
if post_type != POST_TYPE_ALL:
posts = posts.filter(type=post_type)
# filter by topic
if topic_slug:
topic = get_object_or_404(Topic, slug=topic_slug)
posts = posts.filter(topic=topic)
# filter by room
if room_slug:
room = get_object_or_404(Room, slug=room_slug)
posts = posts.filter(room=room)
else:
topic = None
room = None
# filter by label
if label_code:
@ -56,7 +56,7 @@ def feed(request, post_type=POST_TYPE_ALL, topic_slug=None, label_code=None, ord
posts = posts.exclude(is_shadow_banned=True)
# hide no-feed posts (show only inside rooms and topics)
if not topic and not label_code:
if not room and not label_code:
posts = posts.filter(is_visible_in_feeds=True)
# order posts by some metric
@ -71,7 +71,7 @@ def feed(request, post_type=POST_TYPE_ALL, topic_slug=None, label_code=None, ord
return render(request, "feed.html", {
"post_type": post_type or POST_TYPE_ALL,
"ordering": ordering,
"topic": topic,
"room": room,
"label_code": label_code,
"posts": paginate(request, posts),
"pinned_posts": pinned_posts,

View File

@ -241,8 +241,8 @@ def create_or_edit(request, post_type, post=None, mode="create"):
PostSubscription.subscribe(request.me, post, type=PostSubscription.TYPE_ALL_COMMENTS)
if post.is_visible:
if post.topic:
post.topic.update_last_activity()
if post.room:
post.room.update_last_activity()
SearchIndex.update_post_index(post)
LinkedPost.create_links_from_text(post, post.text)

0
rooms/__init__.py Normal file
View File

26
rooms/admin.py Normal file
View File

@ -0,0 +1,26 @@
from django.contrib import admin
from rooms.models import Room
class RoomsAdmin(admin.ModelAdmin):
list_display = (
"slug",
"title",
"subtitle",
"image",
"icon",
"color",
"chat_name",
"network_group",
"last_activity_at",
"is_visible",
"is_open_for_posting",
"is_bot_active",
"index",
)
ordering = ("title",)
search_fields = ["title", "description", "slug"]
admin.site.register(Room, RoomsAdmin)

6
rooms/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RoomsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'rooms'

View File

@ -0,0 +1,72 @@
# Generated by Django 3.2.13 on 2023-07-31 09:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('misc', '0003_alter_networkitem_id'),
]
operations = [
migrations.CreateModel(
name='Room',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False, unique=True)),
('title', models.CharField(max_length=64)),
('subtitle', models.CharField(blank=True, max_length=256, null=True)),
('image', models.URLField()),
('icon', models.URLField(blank=True, null=True)),
('description', models.TextField(blank=True, null=True)),
('color', models.CharField(max_length=16)),
('style', models.CharField(blank=True, default='', max_length=256, null=True)),
('url', models.URLField(blank=True, null=True)),
('chat_name', models.CharField(blank=True, max_length=128, null=True)),
('chat_url', models.URLField(blank=True, null=True)),
('chat_id', models.CharField(blank=True, max_length=32, null=True)),
('last_activity_at', models.DateTimeField(auto_now_add=True)),
('is_visible', models.BooleanField(default=True)),
('is_bot_active', models.BooleanField(default=True)),
('index', models.PositiveIntegerField(default=0)),
('network_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='misc.networkgroup')),
],
options={
'db_table': 'rooms',
'ordering': ['-last_activity_at'],
},
),
migrations.RunSQL("""
insert into rooms (slug, title, subtitle, image, icon, description, color, style, url, chat_name,
chat_url, chat_id, last_activity_at, is_visible, is_bot_active, "index", network_group_id)
select id, name, description, image, icon, '', '#282c35', '', '', name, url, telegram_chat_id, now(),
true, true, index, group_id from network_items;
"""),
migrations.RunSQL("""
insert into rooms (slug, title, subtitle, image, icon, description, color, style, url, chat_name,
chat_url, chat_id, last_activity_at, is_visible, is_bot_active, "index", network_group_id)
select slug, name, '', icon, '', description, color, style, '', chat_name, chat_url, chat_id, now(),
true, false, 0, null from topics where slug in ('nepotism', 'productivity', 'hobby', 'dumbasshome');
"""),
migrations.RunSQL("""
update rooms set (image, description, color, chat_id) = (select icon, description, color, chat_id from topics where slug = 'books') where slug = 'books';
"""),
migrations.RunSQL("""
update rooms set (slug, description, color, chat_id) = (select slug, description, color, chat_id from topics where slug = 'chef') where slug = 'cooking';
"""),
migrations.RunSQL("""
update rooms set (slug, image, description, color, chat_id) = (select slug, icon, description, color, chat_id from topics where slug = 'indie') where slug = 'indiehackers';
"""),
migrations.RunSQL("""
update rooms set (slug, title, image, description, color, chat_id) = (select slug, name, icon, description, color, chat_id from topics where slug = 'stonks') where slug = 'fire';
"""),
migrations.RunSQL("""
update rooms set (image, title, description, color, chat_id) = (select icon, name, description, color, chat_id from topics where slug = 'tractor') where slug = 'tractor';
"""),
migrations.RunSQL("""
update rooms set (image, description, color) = (select icon, description, color from topics where slug = 'travel') where slug = 'travel';
""")
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.13 on 2023-07-31 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rooms', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='room',
options={'ordering': ['index']},
),
migrations.AddField(
model_name='room',
name='is_open_for_posting',
field=models.BooleanField(default=True),
),
]

View File

56
rooms/models.py Normal file
View File

@ -0,0 +1,56 @@
import re
from datetime import datetime, timedelta
from django.db import models
from django.urls import reverse
class Room(models.Model):
slug = models.CharField(primary_key=True, max_length=32, unique=True)
title = models.CharField(max_length=64, null=False)
subtitle = models.CharField(max_length=256, null=True, blank=True)
image = models.URLField(null=False)
icon = models.URLField(null=True, blank=True)
description = models.TextField(null=True, blank=True)
color = models.CharField(max_length=16, null=False)
style = models.CharField(max_length=256, default="", null=True, blank=True)
url = models.URLField(null=True, blank=True)
chat_name = models.CharField(max_length=128, null=True, blank=True)
chat_url = models.URLField(null=True, blank=True)
chat_id = models.CharField(max_length=32, null=True, blank=True)
network_group = models.ForeignKey(
"misc.NetworkGroup",
related_name="rooms",
db_index=True, null=True,
on_delete=models.SET_NULL
)
last_activity_at = models.DateTimeField(auto_now_add=True, null=False)
is_visible = models.BooleanField(default=True)
is_open_for_posting = models.BooleanField(default=True)
is_bot_active = models.BooleanField(default=True)
index = models.PositiveIntegerField(default=0)
class Meta:
db_table = "rooms"
ordering = ["index"]
def __str__(self):
return self.title
def emoji(self):
return re.sub("<.*?>", "", self.icon) if self.icon else ""
def update_last_activity(self):
now = datetime.utcnow()
if self.last_activity_at < now - timedelta(minutes=5):
return Room.objects.filter(slug=self.slug).update(last_activity_at=now)
def get_private_url(self):
if self.url or self.chat_url:
return reverse("redirect_to_room_chat", kwargs={"room_slug": self.slug})
return None

21
rooms/views.py Normal file
View File

@ -0,0 +1,21 @@
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from authn.decorators.auth import require_auth
from rooms.models import Room
@require_auth
def list_rooms(request):
return render(request, "rooms/list_rooms.html")
@require_auth
def redirect_to_room_chat(request, room_slug):
room = get_object_or_404(Room, slug=room_slug)
if room.chat_url:
return redirect(room.chat_url, permanent=False)
elif room.url:
return redirect(room.url, permanent=False)
else:
raise Http404()

View File

@ -77,7 +77,7 @@ class SearchIndex(models.Model):
vector = _multi_search_vector("title", weight="A") \
+ _multi_search_vector("text", weight="B") \
+ _multi_search_vector("author__slug", weight="C") \
+ _multi_search_vector("topic__name", weight="C")
+ _multi_search_vector("room__title", weight="C")
if post.is_searchable:
SearchIndex.objects.update_or_create(