mod: select2 admin and settings constants
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user