mod: select2 admin and settings constants

This commit is contained in:
2026-04-10 11:58:31 +03:00
parent 18e32a2a0f
commit dc84178eba
10 changed files with 381 additions and 18 deletions

View File

@@ -1,9 +1,10 @@
"""Настройки Django для проекта cadpoint.""" """Настройки Django для проекта cadpoint."""
import os
from pathlib import Path from pathlib import Path
from django.db.backends.signals import connection_created
from django.dispatch import receiver
import environ import environ
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -57,6 +58,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# Панель отладки показываем только в dev-окружении при `DEBUG=True`. # Панель отладки показываем только в dev-окружении при `DEBUG=True`.
'debug_toolbar', 'debug_toolbar',
'django_select2',
'easy_thumbnails', 'easy_thumbnails',
'filer.apps.FilerConfig', 'filer.apps.FilerConfig',
'mptt.apps.MpttConfig', 'mptt.apps.MpttConfig',
@@ -157,6 +159,21 @@ CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[])
# Внутренние адреса для debug toolbar: локальный браузер и loopback. # Внутренние адреса для debug toolbar: локальный браузер и loopback.
INTERNAL_IPS = env.list('DJANGO_INTERNAL_IPS', default=['127.0.0.1', '::1']) INTERNAL_IPS = env.list('DJANGO_INTERNAL_IPS', default=['127.0.0.1', '::1'])
# Параметры Select2 в админке.
# Держим их здесь, чтобы не размазывать магические числа по `admin.py`.
SELECT2_AJAX_DELAY_MS = 250
SELECT2_MINIMUM_INPUT_LENGTH = 0
SELECT2_TOKEN_SEPARATORS = '[","]'
SELECT2_PAGE_SIZE = 25
# Параметры SQLite, чтобы дев-окружение не падало на `database is locked`.
# WAL и busy_timeout уменьшают конфликты при чтении/записи, а synchronous=NORMAL
# делает SQLite чуть менее параноидальной, но более живой для локальной разработки.
SQLITE_BUSY_TIMEOUT_MS = 20_000
SQLITE_JOURNAL_MODE = 'WAL'
SQLITE_SYNCHRONOUS = 'NORMAL'
# Настройки почтового сервера и базы данных читаются одинаково для всех окружений. # Настройки почтового сервера и базы данных читаются одинаково для всех окружений.
EMAIL_HOST = env('DJANGO_EMAIL_HOST', default='smtp.mail.ru') # SMTP server EMAIL_HOST = env('DJANGO_EMAIL_HOST', default='smtp.mail.ru') # SMTP server
EMAIL_PORT = env.int('DJANGO_EMAIL_PORT', default=2525) # для SSL/https EMAIL_PORT = env.int('DJANGO_EMAIL_PORT', default=2525) # для SSL/https
@@ -167,14 +184,51 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR.parent.joinpath('database', env('DJANGO_SQLITE_NAME', default='cadpoint-db.sqlite3')), 'NAME': BASE_DIR.parent.joinpath('database', env('DJANGO_SQLITE_NAME', default='cadpoint-db.sqlite3')),
'OPTIONS': {
'timeout': 20,
},
} }
} }
@receiver(connection_created)
def _configure_sqlite_connection(sender, connection, **kwargs):
"""
Настраиваем SQLite сразу после открытия соединения.
Это нужно, чтобы:
- уменьшить число ошибок `database is locked`;
- позволить чтению и записи меньше мешать друг другу;
- сделать dev-среду более терпимой к админке и Select2-поиску.
"""
if connection.vendor != 'sqlite':
return
with connection.cursor() as cursor:
cursor.execute(f'PRAGMA journal_mode={SQLITE_JOURNAL_MODE};')
cursor.execute(f'PRAGMA synchronous={SQLITE_SYNCHRONOUS};')
cursor.execute(f'PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS};')
SERVER_EMAIL = DEFAULT_FROM_EMAIL = EMAIL_FROM SERVER_EMAIL = DEFAULT_FROM_EMAIL = EMAIL_FROM
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_SUBJECT_PREFIX = '[CADPOINT.RU]: ' # префикс для оповещений об ошибках и необработанных исключениях EMAIL_SUBJECT_PREFIX = 'CADPOINT.RU => ' # префикс для оповещений об ошибках и необработанных исключениях
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ============================
# ПЕРЕМЕННЫЕ НАСТРОЙКИ ПРОЕКТА
# Число тегов в облаке на главной странице. Выбирается эмпирически, чтобы не
# перегружать интерфейс и не провоцировать лишние запросы к SQLite при
# открытии страницы.
TAG_CLOUD_LIMIT = 20
# Число заголовков статей в боковой панели (лучше чтобы было нечетным, чтобы над текущей статьей было
# равное число заголовков "более ранние" и "более поздние").
NUM_NAV_ITEMS_IN_PAGE = 7
# Число статей (заголовок + тизер) на странице
NUM_ITEMS_IN_PAGE = NUM_NAV_ITEMS_IN_PAGE

View File

@@ -20,6 +20,11 @@ from cadpoint import settings
from web import views from web import views
urlpatterns = [ urlpatterns = [
path(
settings.ADMIN_URL + 'tags/autocomplete/',
admin.site.admin_view(views.tag_autocomplete),
name='web_tag_autocomplete',
),
path(settings.ADMIN_URL, admin.site.urls), path(settings.ADMIN_URL, admin.site.urls),
re_path(r'^$', views.index), re_path(r'^$', views.index),
re_path(r'^p(?P<ppage>\d*)$', views.index), re_path(r'^p(?P<ppage>\d*)$', views.index),

View File

@@ -1,13 +1,98 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django import forms
from django.contrib import admin from django.contrib import admin
from django.db import models from django.db import models
from django.forms import TextInput, Textarea from django.forms import TextInput, Textarea
from django.urls import reverse
from django_select2.forms import Select2TagWidget
from web.models import TbContent from web.models import TbContent
from web.add_function import safe_html_special_symbols from web.add_function import safe_html_special_symbols
from cadpoint import settings
class AjaxCommaSeparatedSelect2TagWidget(Select2TagWidget):
"""
Select2-виджет для `taggit`.
Select2 в браузере работает с массивом значений, а `taggit` ждёт строку
с тегами через запятую. Поэтому здесь есть конвертация туда и обратно.
"""
def value_from_datadict(self, data, files, name):
# Select2 присылает список значений, а `taggit` ожидает строку вида
# "tag-one,tag two,tag-three".
values = super().value_from_datadict(data, files, name)
if isinstance(values, (list, tuple)):
return ",".join(values)
return values
def optgroups(self, name, value, attrs=None):
# При редактировании объекта нужно показать уже выбранные теги.
# При этом не тащим ВСЕ теги из базы — только те, что уже сохранены.
if isinstance(value, (list, tuple)):
raw_values = []
for item in value:
if not item:
continue
raw_values.extend(str(item).split(","))
else:
raw_values = str(value or "").split(",")
values = [item for item in raw_values if item]
selected = set(values)
subgroup = [
self.create_option(name, v, v, v in selected, i)
for i, v in enumerate(values)
]
return [(None, subgroup, 0)]
class AdminContentForm(forms.ModelForm):
class Meta:
model = TbContent
fields = '__all__'
class Media:
css = {
'all': ('css/admin-select2-theme.css',),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# AJAX-виджет подгружает список тегов лениво, а здесь мы оставляем
# только уже выбранные значения, чтобы не тащить все теги из базы при
# открытии формы и не провоцировать лишние запросы к SQLite.
if self.is_bound:
if hasattr(self.data, 'getlist'):
tag_values = self.data.getlist('tags')
else:
raw_values = self.data.get('tags', [])
tag_values = raw_values if isinstance(raw_values, list) else [raw_values]
tag_choices = [(value, value) for value in tag_values if value]
elif self.instance.pk:
tag_choices = [
(name, name)
for name in self.instance.tags.order_by('name').values_list('name', flat=True)
]
else:
tag_choices = []
self.fields['tags'].widget = AjaxCommaSeparatedSelect2TagWidget(
attrs={
'data-ajax--url': reverse('web_tag_autocomplete'),
'data-ajax--cache': 'true',
'data-ajax--data-type': 'json',
'data-ajax--delay': settings.SELECT2_AJAX_DELAY_MS,
'data-token-separators': settings.SELECT2_TOKEN_SEPARATORS,
'data-minimum-input-length': settings.SELECT2_MINIMUM_INPUT_LENGTH,
},
choices=tag_choices,
)
# Register your models here. # Register your models here.
class AdminContent(admin.ModelAdmin): class AdminContent(admin.ModelAdmin):
form = AdminContentForm
search_fields = ['szContentHead', 'szContentIntro', 'szContentBody', search_fields = ['szContentHead', 'szContentIntro', 'szContentBody',
'szContentKeywords', 'szContentDescription'] 'szContentKeywords', 'szContentDescription']
list_display = ('id', 'ContentHeadSafe', 'tag_list', 'bContentPublish', 'tdContentPublishUp') list_display = ('id', 'ContentHeadSafe', 'tag_list', 'bContentPublish', 'tdContentPublishUp')
@@ -49,7 +134,10 @@ class AdminContent(admin.ModelAdmin):
return safe_html_special_symbols(obj.szContentHead) return safe_html_special_symbols(obj.szContentHead)
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags') queryset = super().get_queryset(request)
if request.resolver_match and request.resolver_match.url_name == 'web_tbcontent_changelist':
return queryset.prefetch_related('tags')
return queryset
def tag_list(self, obj): def tag_list(self, obj):
return u", ".join(o.name for o in obj.tags.all()) return u", ".join(o.name for o in obj.tags.all())

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.13 on 2026-04-09 12:21
import taggit.managers
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
('web', '0001_squashed_0002_alter_tbcontent_szcontentbody_and_more'),
]
operations = [
migrations.AlterField(
model_name='tbcontent',
name='tags',
field=taggit.managers.TaggableManager(blank=True, help_text='Теги можно выбирать из списка или вводить вручную. Многословные теги поддерживаются без кавычек. <b>Теги нужны для присвоения категорий объектам контента<b>.', through='web.RuTaggedItem', to='taggit.Tag', verbose_name='Теги'),
),
]

View File

@@ -73,8 +73,8 @@ class TbContent(models.Model):
blank=True, blank=True,
through=RuTaggedItem, # uTaggedItem, through=RuTaggedItem, # uTaggedItem,
verbose_name=u"Теги", verbose_name=u"Теги",
help_text=u"Теги через запятую… Регистр не чувствителен… Длинные теги, содержащие пробел, заключайте" help_text=u"Теги можно выбирать из списка или вводить вручную. Многословные теги поддерживаются"
u"'в кавычки' <b>Теги нужны для присвоения категорий объектам контента<b>." u" без кавычек. <b>Теги нужны для присвоения категорий объектам контента<b>."
) )
szContentHead = models.CharField( szContentHead = models.CharField(
max_length=512, default=u"", blank=False, null=False, max_length=512, default=u"", blank=False, null=False,

View File

@@ -1,4 +1,7 @@
from django.test import SimpleTestCase from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from taggit.models import Tag
from web.legacy_links import build_canonical_url, replace_legacy_links from web.legacy_links import build_canonical_url, replace_legacy_links
@@ -31,3 +34,54 @@ class LegacyLinksTests(SimpleTestCase):
self.assertIn('/images/stories/news/photo123.jpg', new_text) self.assertIn('/images/stories/news/photo123.jpg', new_text)
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
class TagAutocompleteTests(TestCase):
def setUp(self):
user_model = get_user_model()
self.user = user_model.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password',
)
Tag.objects.create(name='alpha')
Tag.objects.create(name='beta')
Tag.objects.create(name='gamma')
self.client.force_login(self.user)
def test_returns_tag_results_for_term(self):
response = self.client.get(
reverse('web_tag_autocomplete'),
{'term': 'al'},
)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['pagination']['more'], False)
self.assertEqual([item['text'] for item in payload['results']], ['alpha'])
def test_returns_initial_tag_batch_without_term(self):
response = self.client.get(reverse('web_tag_autocomplete'))
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 3)
self.assertEqual(payload['pagination']['more'], False)
def test_paginates_tag_results(self):
Tag.objects.all().delete()
for index in range(30):
Tag.objects.create(name=f'tag-{index:02d}')
response = self.client.get(reverse('web_tag_autocomplete'), {'page': 1})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 25)
self.assertEqual(payload['pagination']['more'], True)
response = self.client.get(reverse('web_tag_autocomplete'), {'page': 2})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 5)
self.assertEqual(payload['pagination']['more'], False)

View File

@@ -2,14 +2,16 @@
import math import math
from django.shortcuts import render, HttpResponseRedirect from django.shortcuts import render, HttpResponseRedirect
from django.http import Http404 from django.http import Http404, JsonResponse
from django.db.models import Count, Q from django.db.models import Count, Q
from django.views.decorators.http import require_GET
# from datetime import datetime # from datetime import datetime
from django.utils import timezone from django.utils import timezone
from taggit.models import Tag from taggit.models import Tag
from web.models import TbContent from web.models import TbContent
from web.add_function import * from web.add_function import *
from cadpoint import settings
# Create your views here. # Create your views here.
def handler404(request, exception: str): def handler404(request, exception: str):
@@ -35,6 +37,29 @@ def handler500(request):
return response return response
@require_GET
def tag_autocomplete(request):
"""Отдаёт теги для Select2 лениво, чтобы не грузить всю таблицу сразу."""
term = request.GET.get("term", "").strip()
page = max(int(request.GET.get("page", 1)), 1)
page_size = settings.SELECT2_PAGE_SIZE
queryset = Tag.objects.order_by("name")
if term:
queryset = queryset.filter(name__icontains=term)
start = (page - 1) * page_size
stop = start + page_size + 1
names = list(queryset.values_list("name", flat=True)[start:stop])
more = len(names) > page_size
results = [
{"id": name, "text": name}
for name in names[:page_size]
]
return JsonResponse({"results": results, "pagination": {"more": more}})
def index(request, def index(request,
slug_tags: str = "", slug_tags: str = "",
ppage: int = 0): ppage: int = 0):
@@ -71,9 +96,10 @@ def index(request,
q_content = content_qs.order_by("-tdContentPublishUp") q_content = content_qs.order_by("-tdContentPublishUp")
total_items = q_content.count() total_items = q_content.count()
total_page = max(math.ceil(total_items / 7) - 1, 0) if total_items else 0 total_page = max(math.ceil(total_items / settings.NUM_ITEMS_IN_PAGE) - 1, 0) if total_items else 0
q_content = q_content[page_number * 7: page_number * 7 + 7] q_content = q_content[page_number * settings.NUM_ITEMS_IN_PAGE:
page_number * settings.NUM_ITEMS_IN_PAGE+ settings.NUM_ITEMS_IN_PAGE]
# Готовим облако тегов: общее число публикаций по каждому тегу и число публикаций на текущей странице. # Готовим облако тегов: общее число публикаций по каждому тегу и число публикаций на текущей странице.
page_ids = list(q_content.values_list("id", flat=True)) page_ids = list(q_content.values_list("id", flat=True))
@@ -87,7 +113,7 @@ def index(request,
), ),
) )
.filter(NumTotal__gt=0) .filter(NumTotal__gt=0)
.order_by("-NumInPage", "-NumTotal", "name")[:20] .order_by("-NumInPage", "-NumTotal", "name")[:settings.TAG_CLOUD_LIMIT]
) )
to_template["LENTA"] = q_content to_template["LENTA"] = q_content
@@ -136,12 +162,12 @@ def show_item(request,
Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()), Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()),
Q(bContentPublish=True), Q(bContentPublish=True),
Q(tdContentPublishUp__lte=q_item.tdContentPublishUp) Q(tdContentPublishUp__lte=q_item.tdContentPublishUp)
).order_by("-tdContentPublishUp", "id")[:4] ).order_by("-tdContentPublishUp", "id")[:settings.NUM_NAV_ITEMS_IN_PAGE / 2]
q_items_before = TbContent.objects.filter( q_items_before = TbContent.objects.filter(
Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()), Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()),
bContentPublish=True, bContentPublish=True,
tdContentPublishUp__gt=q_item.tdContentPublishUp tdContentPublishUp__gt=q_item.tdContentPublishUp
).order_by("tdContentPublishUp", "id")[:3] ).order_by("tdContentPublishUp", "id")[:settings.NUM_NAV_ITEMS_IN_PAGE / 2 - 1]
try: try:
p = 0 if "p" not in request.GET else int(request.GET["p"]) p = 0 if "p" not in request.GET else int(request.GET["p"])
n = 0 if "n" not in request.GET else int(request.GET["n"]) n = 0 if "n" not in request.GET else int(request.GET["n"])
@@ -150,7 +176,7 @@ def show_item(request,
count += 1 count += 1
if n-count < 1: if n-count < 1:
i.pp = p - 1 i.pp = p - 1
i.nn = n + 7 - count i.nn = n + settings.NUM_NAV_ITEMS_IN_PAGE - count
else: else:
i.pp = p i.pp = p
i.nn = n - count i.nn = n - count
@@ -158,13 +184,13 @@ def show_item(request,
for i in q_items_after: for i in q_items_after:
if i.id != q_item.id: if i.id != q_item.id:
count += 1 count += 1
if n+count <= 7: if n+count <= settings.NUM_NAV_ITEMS_IN_PAGE:
i.pp = p i.pp = p
i.nn = n + count i.nn = n + count
else: else:
i.pp = p + 1 i.pp = p + 1
i.nn = n+count - 7 i.nn = n+count - settings.NUM_NAV_ITEMS_IN_PAGE
to_template["PER_PAGE"] = 7 to_template["PER_PAGE"] = settings.NUM_NAV_ITEMS_IN_PAGE
to_template["PAGE"] = p to_template["PAGE"] = p
except ValueError: except ValueError:
to_template["PAGE"] = 0 to_template["PAGE"] = 0

31
poetry.lock generated
View File

@@ -191,6 +191,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"] bcrypt = ["bcrypt"]
[[package]]
name = "django-appconf"
version = "1.2.0"
description = "A helper class for handling configuration defaults of packaged apps gracefully."
optional = false
python-versions = ">=3.9"
files = [
{file = "django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4"},
{file = "django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec"},
]
[package.dependencies]
django = "*"
[[package]] [[package]]
name = "django-debug-toolbar" name = "django-debug-toolbar"
version = "6.3.0" version = "6.3.0"
@@ -291,6 +305,21 @@ files = [
django = ">=4.2" django = ">=4.2"
typing-extensions = ">=4.12.0" typing-extensions = ">=4.12.0"
[[package]]
name = "django-select2"
version = "8.4.8"
description = "This is a Django_ integration of Select2_."
optional = false
python-versions = ">=3.10"
files = [
{file = "django_select2-8.4.8-py3-none-any.whl", hash = "sha256:a2ce6a4c556dd2d4d57eb3753618d6f31f8d3910e9d9fa1b686d9340f50b14eb"},
{file = "django_select2-8.4.8.tar.gz", hash = "sha256:592e52effff2b5850cb7c98b265715b6704fb784699c4aedddfdd8ae1ffa1e81"},
]
[package.dependencies]
django = ">=4.2"
django-appconf = ">=0.6.0"
[[package]] [[package]]
name = "django-taggit" name = "django-taggit"
version = "6.1.0" version = "6.1.0"
@@ -743,4 +772,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.12,<3.13" python-versions = ">=3.12,<3.13"
content-hash = "c8a9496d279767e1ce179c985beb1f1813933c600163f061866c70fa413467d0" content-hash = "8115895b95f66275813106c93c64640813e5939a984e76ad53a17032f0788de0"

View File

@@ -0,0 +1,86 @@
/* Стили Select2 для админки Django 5.
Используем CSS-переменные админки, чтобы оформление автоматически
подстраивалось под светлую, тёмную и auto-тему. */
.select2-container--default .select2-selection--single,
.select2-container--default .select2-selection--multiple {
min-height: 2.5rem;
border: 1px solid var(--border-color) !important;
border-radius: 4px;
background-color: var(--body-bg) !important;
color: var(--body-fg) !important;
}
.select2-container--default .select2-selection--single .select2-selection__rendered,
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
color: var(--body-fg) !important;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder,
.select2-container--default .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color) !important;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 2.4rem;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
border: 1px solid var(--border-color) !important;
border-radius: 999px;
background-color: var(--selected-bg) !important;
color: var(--body-fg) !important;
padding: 0.15rem 0.55rem;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color) !important;
margin-right: 0.35rem;
}
.select2-container--default .select2-search--inline .select2-search__field,
.select2-container--default .select2-search--dropdown .select2-search__field {
color: var(--body-fg) !important;
border: 1px solid var(--border-color) !important;
background-color: var(--body-bg) !important;
}
.select2-container--default .select2-results > .select2-results__options {
background-color: var(--body-bg) !important;
color: var(--body-fg) !important;
}
.select2-container--default .select2-results__option {
color: var(--body-fg) !important;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary) !important;
color: var(--primary-fg) !important;
}
.select2-container--default .select2-results__option[aria-selected="true"] {
background-color: var(--selected-bg) !important;
color: var(--body-fg) !important;
}
.select2-container--default.select2-container--focus .select2-selection--multiple,
.select2-container--default.select2-container--open .select2-selection--single,
.select2-container--default.select2-container--open .select2-selection--multiple {
border-color: var(--primary) !important;
}
.select2-dropdown {
border: 1px solid var(--border-color) !important;
background-color: var(--body-bg) !important;
color: var(--body-fg) !important;
}
.select2-container--default .select2-results__option--disabled {
color: var(--body-quiet-color) !important;
}
.select2-container--default .select2-results__group {
color: var(--body-quiet-color) !important;
}

View File

@@ -22,6 +22,7 @@ setuptools = "^82.0"
django-environ = "^0.13" django-environ = "^0.13"
django-mptt = "^0.18.0" django-mptt = "^0.18.0"
pytils = "^0.4.4" pytils = "^0.4.4"
django-select2 = "^8.4.8"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^6.3" django-debug-toolbar = "^6.3"