mod: select2 admin and settings constants
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"""Настройки Django для проекта cadpoint."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.db.backends.signals import connection_created
|
||||
from django.dispatch import receiver
|
||||
import environ
|
||||
import os
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
@@ -57,6 +58,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
# Панель отладки показываем только в dev-окружении при `DEBUG=True`.
|
||||
'debug_toolbar',
|
||||
'django_select2',
|
||||
'easy_thumbnails',
|
||||
'filer.apps.FilerConfig',
|
||||
'mptt.apps.MpttConfig',
|
||||
@@ -157,6 +159,21 @@ CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[])
|
||||
# Внутренние адреса для debug toolbar: локальный браузер и loopback.
|
||||
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_PORT = env.int('DJANGO_EMAIL_PORT', default=2525) # для SSL/https
|
||||
@@ -167,14 +184,51 @@ DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.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
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_SUBJECT_PREFIX = '[CADPOINT.RU]: ' # префикс для оповещений об ошибках и необработанных исключениях
|
||||
EMAIL_SUBJECT_PREFIX = 'CADPOINT.RU => ' # префикс для оповещений об ошибках и необработанных исключениях
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ from cadpoint import settings
|
||||
from web import views
|
||||
|
||||
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),
|
||||
re_path(r'^$', views.index),
|
||||
re_path(r'^p(?P<ppage>\d*)$', views.index),
|
||||
|
||||
@@ -1,13 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.forms import TextInput, Textarea
|
||||
from django.urls import reverse
|
||||
from django_select2.forms import Select2TagWidget
|
||||
from web.models import TbContent
|
||||
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.
|
||||
class AdminContent(admin.ModelAdmin):
|
||||
form = AdminContentForm
|
||||
search_fields = ['szContentHead', 'szContentIntro', 'szContentBody',
|
||||
'szContentKeywords', 'szContentDescription']
|
||||
list_display = ('id', 'ContentHeadSafe', 'tag_list', 'bContentPublish', 'tdContentPublishUp')
|
||||
@@ -49,7 +134,10 @@ class AdminContent(admin.ModelAdmin):
|
||||
return safe_html_special_symbols(obj.szContentHead)
|
||||
|
||||
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):
|
||||
return u", ".join(o.name for o in obj.tags.all())
|
||||
|
||||
20
cadpoint/web/migrations/0003_alter_tbcontent_tags.py
Normal file
20
cadpoint/web/migrations/0003_alter_tbcontent_tags.py
Normal 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='Теги'),
|
||||
),
|
||||
]
|
||||
@@ -73,8 +73,8 @@ class TbContent(models.Model):
|
||||
blank=True,
|
||||
through=RuTaggedItem, # uTaggedItem,
|
||||
verbose_name=u"Теги",
|
||||
help_text=u"Теги через запятую… Регистр не чувствителен… Длинные теги, содержащие пробел, заключайте"
|
||||
u"'в кавычки'… <b>Теги нужны для присвоения категорий объектам контента<b>."
|
||||
help_text=u"Теги можно выбирать из списка или вводить вручную. Многословные теги поддерживаются"
|
||||
u" без кавычек. <b>Теги нужны для присвоения категорий объектам контента<b>."
|
||||
)
|
||||
szContentHead = models.CharField(
|
||||
max_length=512, default=u"", blank=False, null=False,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,3 +34,54 @@ class LegacyLinksTests(SimpleTestCase):
|
||||
self.assertIn('/images/stories/news/photo123.jpg', new_text)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
import math
|
||||
|
||||
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.views.decorators.http import require_GET
|
||||
# from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from taggit.models import Tag
|
||||
|
||||
from web.models import TbContent
|
||||
from web.add_function import *
|
||||
from cadpoint import settings
|
||||
|
||||
# Create your views here.
|
||||
def handler404(request, exception: str):
|
||||
@@ -35,6 +37,29 @@ def handler500(request):
|
||||
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,
|
||||
slug_tags: str = "",
|
||||
ppage: int = 0):
|
||||
@@ -71,9 +96,10 @@ def index(request,
|
||||
|
||||
q_content = content_qs.order_by("-tdContentPublishUp")
|
||||
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))
|
||||
@@ -87,7 +113,7 @@ def index(request,
|
||||
),
|
||||
)
|
||||
.filter(NumTotal__gt=0)
|
||||
.order_by("-NumInPage", "-NumTotal", "name")[:20]
|
||||
.order_by("-NumInPage", "-NumTotal", "name")[:settings.TAG_CLOUD_LIMIT]
|
||||
)
|
||||
|
||||
to_template["LENTA"] = q_content
|
||||
@@ -136,12 +162,12 @@ def show_item(request,
|
||||
Q(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()),
|
||||
Q(bContentPublish=True),
|
||||
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(tdContentPublishDown__isnull=True) | Q(tdContentPublishDown__gt=timezone.now()),
|
||||
bContentPublish=True,
|
||||
tdContentPublishUp__gt=q_item.tdContentPublishUp
|
||||
).order_by("tdContentPublishUp", "id")[:3]
|
||||
).order_by("tdContentPublishUp", "id")[:settings.NUM_NAV_ITEMS_IN_PAGE / 2 - 1]
|
||||
try:
|
||||
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"])
|
||||
@@ -150,7 +176,7 @@ def show_item(request,
|
||||
count += 1
|
||||
if n-count < 1:
|
||||
i.pp = p - 1
|
||||
i.nn = n + 7 - count
|
||||
i.nn = n + settings.NUM_NAV_ITEMS_IN_PAGE - count
|
||||
else:
|
||||
i.pp = p
|
||||
i.nn = n - count
|
||||
@@ -158,13 +184,13 @@ def show_item(request,
|
||||
for i in q_items_after:
|
||||
if i.id != q_item.id:
|
||||
count += 1
|
||||
if n+count <= 7:
|
||||
if n+count <= settings.NUM_NAV_ITEMS_IN_PAGE:
|
||||
i.pp = p
|
||||
i.nn = n + count
|
||||
else:
|
||||
i.pp = p + 1
|
||||
i.nn = n+count - 7
|
||||
to_template["PER_PAGE"] = 7
|
||||
i.nn = n+count - settings.NUM_NAV_ITEMS_IN_PAGE
|
||||
to_template["PER_PAGE"] = settings.NUM_NAV_ITEMS_IN_PAGE
|
||||
to_template["PAGE"] = p
|
||||
except ValueError:
|
||||
to_template["PAGE"] = 0
|
||||
|
||||
31
poetry.lock
generated
31
poetry.lock
generated
@@ -191,6 +191,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
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]]
|
||||
name = "django-debug-toolbar"
|
||||
version = "6.3.0"
|
||||
@@ -291,6 +305,21 @@ files = [
|
||||
django = ">=4.2"
|
||||
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]]
|
||||
name = "django-taggit"
|
||||
version = "6.1.0"
|
||||
@@ -743,4 +772,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.12,<3.13"
|
||||
content-hash = "c8a9496d279767e1ce179c985beb1f1813933c600163f061866c70fa413467d0"
|
||||
content-hash = "8115895b95f66275813106c93c64640813e5939a984e76ad53a17032f0788de0"
|
||||
|
||||
86
public/static/css/admin-select2-theme.css
Normal file
86
public/static/css/admin-select2-theme.css
Normal 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;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ setuptools = "^82.0"
|
||||
django-environ = "^0.13"
|
||||
django-mptt = "^0.18.0"
|
||||
pytils = "^0.4.4"
|
||||
django-select2 = "^8.4.8"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
django-debug-toolbar = "^6.3"
|
||||
|
||||
Reference in New Issue
Block a user