mod: виртуальные поля типографа
This commit is contained in:
@@ -5,11 +5,25 @@ from django.db import models
|
|||||||
from django.forms import TextInput, Textarea
|
from django.forms import TextInput, Textarea
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_select2.forms import Select2TagWidget
|
from django_select2.forms import Select2TagWidget
|
||||||
|
from etpgrf.config import MODE_MIXED, MODE_MNEMONIC, MODE_UNICODE, SANITIZE_ALL_HTML, SANITIZE_ETPGRF
|
||||||
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
|
from cadpoint import settings
|
||||||
|
|
||||||
|
|
||||||
|
TYPOGRAPH_MODE_CHOICES = [
|
||||||
|
(MODE_MIXED, 'Смешанный (Mixed)'),
|
||||||
|
(MODE_UNICODE, 'Юникод (Unicode)'),
|
||||||
|
(MODE_MNEMONIC, 'Мнемоники'),
|
||||||
|
]
|
||||||
|
|
||||||
|
TYPOGRAPH_SANITIZER_CHOICES = [
|
||||||
|
(SANITIZE_ALL_HTML, 'Очистка от HTML на входе'),
|
||||||
|
(SANITIZE_ETPGRF, 'Очистка висячей пунктуации'),
|
||||||
|
('None', 'Без очистки'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AjaxCommaSeparatedSelect2TagWidget(Select2TagWidget):
|
class AjaxCommaSeparatedSelect2TagWidget(Select2TagWidget):
|
||||||
"""
|
"""
|
||||||
Select2-виджет для `taggit`.
|
Select2-виджет для `taggit`.
|
||||||
@@ -48,9 +62,46 @@ class AjaxCommaSeparatedSelect2TagWidget(Select2TagWidget):
|
|||||||
|
|
||||||
|
|
||||||
class AdminContentForm(forms.ModelForm):
|
class AdminContentForm(forms.ModelForm):
|
||||||
|
typograph_enabled = forms.BooleanField(
|
||||||
|
label='Типограф etpgrf вкл.',
|
||||||
|
required=False,
|
||||||
|
initial=True,
|
||||||
|
help_text="Обработать через <a href=\"https://typograph.cube2.ru/\""
|
||||||
|
" target=\"_blank\">Типограф ETPRGF</a><br />"
|
||||||
|
"<small><u>СТАБИЛЬНЫЙ И СОВРЕМЕННЫЙ ТИПОГРАФ, РЕКОМЕНДУЕМ</u><br />"
|
||||||
|
"«приклеивает» союзы и предлоги, поддерживает неразрывные конструкции, "
|
||||||
|
"замена тире, кавычек и дефисов, расстановка «мягких переносов» "
|
||||||
|
"в словах длиннее 14 символов, <!-- убирает «вдовы» «сироты» (кроме "
|
||||||
|
"заголовков), расставляет абзацы (кроме заголовков), расшифровывает "
|
||||||
|
"аббревиатуры (те, что знает и кроме заголовков), --> висячая "
|
||||||
|
"пунктуация (только в заголовках) и т.п.</small>"
|
||||||
|
)
|
||||||
|
typograph_strip_soft_hyphens = forms.BooleanField(
|
||||||
|
label='Удалять переносы',
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
help_text='Убирает `&shy;`, `&#173;` и Unicode-символ мягкого переноса<br />'
|
||||||
|
'перед типографом.',
|
||||||
|
)
|
||||||
|
typograph_mode = forms.ChoiceField(
|
||||||
|
label='Режим вывода',
|
||||||
|
choices=TYPOGRAPH_MODE_CHOICES,
|
||||||
|
initial=MODE_MIXED,
|
||||||
|
)
|
||||||
|
typograph_hyphenation = forms.BooleanField(
|
||||||
|
label='Расстановка переносов',
|
||||||
|
required=False,
|
||||||
|
initial=True,
|
||||||
|
)
|
||||||
|
typograph_sanitizer = forms.ChoiceField(
|
||||||
|
label='Санитайзинг',
|
||||||
|
choices=TYPOGRAPH_SANITIZER_CHOICES,
|
||||||
|
initial='None',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TbContent
|
model = TbContent
|
||||||
fields = '__all__'
|
exclude = ('bTypograf',)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
@@ -59,6 +110,7 @@ class AdminContentForm(forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['typograph_enabled'].initial = self.instance.bTypograf
|
||||||
# AJAX-виджет подгружает список тегов лениво, а здесь мы оставляем
|
# AJAX-виджет подгружает список тегов лениво, а здесь мы оставляем
|
||||||
# только уже выбранные значения, чтобы не тащить все теги из базы при
|
# только уже выбранные значения, чтобы не тащить все теги из базы при
|
||||||
# открытии формы и не провоцировать лишние запросы к SQLite.
|
# открытии формы и не провоцировать лишние запросы к SQLite.
|
||||||
@@ -117,7 +169,11 @@ class AdminContent(admin.ModelAdmin):
|
|||||||
'fields': ('tags', 'szContentHead', 'imgContentPreview', 'szContentIntro', 'szContentBody')
|
'fields': ('tags', 'szContentHead', 'imgContentPreview', 'szContentIntro', 'szContentBody')
|
||||||
}),
|
}),
|
||||||
('Типограф', {
|
('Типограф', {
|
||||||
'fields': ('bTypograf', ),
|
'fields': (
|
||||||
|
('typograph_enabled', ),
|
||||||
|
('typograph_mode', 'typograph_sanitizer', ),
|
||||||
|
('typograph_strip_soft_hyphens', 'typograph_hyphenation', ),
|
||||||
|
),
|
||||||
'classes': ('collapse',),
|
'classes': ('collapse',),
|
||||||
}),
|
}),
|
||||||
('Поля для SEO', {
|
('Поля для SEO', {
|
||||||
@@ -130,6 +186,14 @@ class AdminContent(admin.ModelAdmin):
|
|||||||
actions_on_top = False
|
actions_on_top = False
|
||||||
actions_on_bottom = False
|
actions_on_bottom = False
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
obj.bTypograf = form.cleaned_data.get('typograph_enabled', False)
|
||||||
|
obj._typograph_strip_soft_hyphens = form.cleaned_data.get('typograph_strip_soft_hyphens', True)
|
||||||
|
obj._typograph_mode = form.cleaned_data.get('typograph_mode', MODE_MIXED)
|
||||||
|
obj._typograph_hyphenation = form.cleaned_data.get('typograph_hyphenation', True)
|
||||||
|
obj._typograph_sanitizer = form.cleaned_data.get('typograph_sanitizer', 'None')
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
def ContentHeadSafe(self, obj) -> str:
|
def ContentHeadSafe(self, obj) -> str:
|
||||||
return safe_html_special_symbols(obj.szContentHead)
|
return safe_html_special_symbols(obj.szContentHead)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import logging
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from etpgrf import Hyphenator, Typographer
|
from etpgrf import Hyphenator, Typographer
|
||||||
|
from etpgrf.config import (
|
||||||
|
MODE_MIXED,
|
||||||
|
MODE_MNEMONIC,
|
||||||
|
MODE_UNICODE,
|
||||||
|
SANITIZE_ALL_HTML,
|
||||||
|
SANITIZE_ETPGRF,
|
||||||
|
SANITIZE_NONE,
|
||||||
|
)
|
||||||
from filer.fields.image import FilerFileField
|
from filer.fields.image import FilerFileField
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from taggit.models import Tag, TaggedItem
|
from taggit.models import Tag, TaggedItem
|
||||||
@@ -20,15 +28,61 @@ logger = logging.getLogger(__name__)
|
|||||||
_TYPOGRAPHER_LANGS = 'ru+en'
|
_TYPOGRAPHER_LANGS = 'ru+en'
|
||||||
_TYPOGRAPHER_MAX_UNHYPHENATED_LEN = 14
|
_TYPOGRAPHER_MAX_UNHYPHENATED_LEN = 14
|
||||||
|
|
||||||
def _build_typographer(hanging_punctuation=None) -> Typographer:
|
_TYPOGRAPHER_DEFAULT_MODE = MODE_MIXED
|
||||||
|
_TYPOGRAPHER_DEFAULT_HYPHENATION = True
|
||||||
|
_TYPOGRAPHER_DEFAULT_SANITIZER = SANITIZE_NONE
|
||||||
|
_TYPOGRAPHER_DEFAULT_STRIP_SOFT_HYPHENS = True
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_typograph_mode(value: str | None) -> str:
|
||||||
|
if value in {MODE_MIXED, MODE_UNICODE, MODE_MNEMONIC}:
|
||||||
|
return str(value)
|
||||||
|
return _TYPOGRAPHER_DEFAULT_MODE
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_typograph_hyphenation(value) -> bool:
|
||||||
|
return bool(_TYPOGRAPHER_DEFAULT_HYPHENATION if value is None else value)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_typograph_sanitizer(value):
|
||||||
|
if value in (None, '', 'None', SANITIZE_NONE):
|
||||||
|
return SANITIZE_NONE
|
||||||
|
if value == SANITIZE_ALL_HTML:
|
||||||
|
return SANITIZE_ALL_HTML
|
||||||
|
if value == SANITIZE_ETPGRF:
|
||||||
|
return SANITIZE_ETPGRF
|
||||||
|
return SANITIZE_NONE
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_soft_hyphens(text: str) -> str:
|
||||||
|
"""Удаляет мягкие переносы в любом виде перед передачей текста в etpgrf."""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
return (
|
||||||
|
text
|
||||||
|
.replace("­", "")
|
||||||
|
.replace("­", "")
|
||||||
|
.replace("­", "")
|
||||||
|
.replace("\u00ad", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_typographer(mode=None, hyphenation=True, sanitizer=None, hanging_punctuation=None) -> Typographer:
|
||||||
"""Собирает `etpgrf` с едиными настройками для заголовка и текста."""
|
"""Собирает `etpgrf` с едиными настройками для заголовка и текста."""
|
||||||
|
normalized_mode = _normalize_typograph_mode(mode)
|
||||||
|
normalized_hyphenation = _normalize_typograph_hyphenation(hyphenation)
|
||||||
|
normalized_sanitizer = _normalize_typograph_sanitizer(sanitizer)
|
||||||
return Typographer(
|
return Typographer(
|
||||||
langs=_TYPOGRAPHER_LANGS,
|
langs=_TYPOGRAPHER_LANGS,
|
||||||
|
mode=normalized_mode,
|
||||||
process_html=True,
|
process_html=True,
|
||||||
hyphenation=Hyphenator(
|
hyphenation=(
|
||||||
langs=_TYPOGRAPHER_LANGS,
|
Hyphenator(
|
||||||
max_unhyphenated_len=_TYPOGRAPHER_MAX_UNHYPHENATED_LEN,
|
langs=_TYPOGRAPHER_LANGS,
|
||||||
|
max_unhyphenated_len=_TYPOGRAPHER_MAX_UNHYPHENATED_LEN,
|
||||||
|
) if normalized_hyphenation else False
|
||||||
),
|
),
|
||||||
|
sanitizer=normalized_sanitizer,
|
||||||
hanging_punctuation=hanging_punctuation,
|
hanging_punctuation=hanging_punctuation,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,7 +136,7 @@ class TbContent(models.Model):
|
|||||||
# | szContentTitle -- title для SEO | longtext NOT NULL,
|
# | szContentTitle -- title для SEO | longtext NOT NULL,
|
||||||
# | szContentKeywords -- keywords для SEO | longtext NOT NULL,
|
# | szContentKeywords -- keywords для SEO | longtext NOT NULL,
|
||||||
# | szContentDescription -- Description для SEO | longtext NOT NULL,
|
# | szContentDescription -- Description для SEO | longtext NOT NULL,
|
||||||
# | dtContentCreate -- дата и время создания | datetime(6) NOT NULL,
|
# | dtContentCreate -- дата и время создания | datetime(6) NOT NULL,
|
||||||
# | dtContentTimeStamp -- штамп времени (время последнего обновления в базе) | datetime(6) NOT NULL
|
# | dtContentTimeStamp -- штамп времени (время последнего обновления в базе) | datetime(6) NOT NULL
|
||||||
# ============================================================
|
# ============================================================
|
||||||
bContentPublish = models.BooleanField(
|
bContentPublish = models.BooleanField(
|
||||||
@@ -145,6 +199,7 @@ class TbContent(models.Model):
|
|||||||
verbose_name="◉",
|
verbose_name="◉",
|
||||||
help_text="Число просмотров"
|
help_text="Число просмотров"
|
||||||
)
|
)
|
||||||
|
# Поле для удаления. Все будет делаться с помощью виртуальных полей админки
|
||||||
bTypograf = models.BooleanField(
|
bTypograf = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name="Типограф etpgrf",
|
verbose_name="Типограф etpgrf",
|
||||||
@@ -193,6 +248,14 @@ class TbContent(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Переопределяем save(), чтобы автоматически типографировать контент перед сохранением.
|
# Переопределяем save(), чтобы автоматически типографировать контент перед сохранением.
|
||||||
|
typograph_mode = getattr(self, '_typograph_mode', _TYPOGRAPHER_DEFAULT_MODE)
|
||||||
|
typograph_hyphenation = getattr(self, '_typograph_hyphenation', _TYPOGRAPHER_DEFAULT_HYPHENATION)
|
||||||
|
typograph_sanitizer = getattr(self, '_typograph_sanitizer', _TYPOGRAPHER_DEFAULT_SANITIZER)
|
||||||
|
typograph_strip_soft_hyphens = getattr(
|
||||||
|
self,
|
||||||
|
'_typograph_strip_soft_hyphens',
|
||||||
|
_TYPOGRAPHER_DEFAULT_STRIP_SOFT_HYPHENS,
|
||||||
|
)
|
||||||
if self.szContentSlug is None or self.szContentSlug == "" or " " in self.szContentSlug:
|
if self.szContentSlug is None or self.szContentSlug == "" or " " in self.szContentSlug:
|
||||||
# print("ку-ку", self.szContentHead)
|
# print("ку-ку", self.szContentHead)
|
||||||
base_slug = clean_text_to_slug(self.szContentHead)
|
base_slug = clean_text_to_slug(self.szContentHead)
|
||||||
@@ -205,11 +268,29 @@ class TbContent(models.Model):
|
|||||||
if self.bTypograf:
|
if self.bTypograf:
|
||||||
# `etpgrf` уже умеет HTML-режим и висячую пунктуацию, поэтому здесь
|
# `etpgrf` уже умеет HTML-режим и висячую пунктуацию, поэтому здесь
|
||||||
# не нужен старый локальный fallback.
|
# не нужен старый локальный fallback.
|
||||||
|
# Мягкие переносы убираем заранее: `etpgrf` не очищает их сам, а они
|
||||||
|
# потом мешают и типографу, и последующей нормализации текста.
|
||||||
# Для заголовка включаем левую висячую пунктуацию, а для анонса и
|
# Для заголовка включаем левую висячую пунктуацию, а для анонса и
|
||||||
# тела текста оставляем обычную обработку без hanging punctuation.
|
# тела текста оставляем обычную обработку без hanging punctuation.
|
||||||
self.szContentHead = _typograph_text(self.szContentHead, _TYPOGRAPHER_HEAD)
|
if typograph_strip_soft_hyphens:
|
||||||
self.szContentIntro = _typograph_text(self.szContentIntro, _TYPOGRAPHER_TEXT)
|
self.szContentHead = _strip_soft_hyphens(self.szContentHead)
|
||||||
self.szContentBody = _typograph_text(self.szContentBody, _TYPOGRAPHER_TEXT)
|
self.szContentIntro = _strip_soft_hyphens(self.szContentIntro)
|
||||||
|
self.szContentBody = _strip_soft_hyphens(self.szContentBody)
|
||||||
|
head_typographer = _build_typographer(
|
||||||
|
mode=typograph_mode,
|
||||||
|
hyphenation=typograph_hyphenation,
|
||||||
|
sanitizer=typograph_sanitizer,
|
||||||
|
hanging_punctuation='left',
|
||||||
|
)
|
||||||
|
text_typographer = _build_typographer(
|
||||||
|
mode=typograph_mode,
|
||||||
|
hyphenation=typograph_hyphenation,
|
||||||
|
sanitizer=typograph_sanitizer,
|
||||||
|
hanging_punctuation=False,
|
||||||
|
)
|
||||||
|
self.szContentHead = _typograph_text(self.szContentHead, head_typographer)
|
||||||
|
self.szContentIntro = _typograph_text(self.szContentIntro, text_typographer)
|
||||||
|
self.szContentBody = _typograph_text(self.szContentBody, text_typographer)
|
||||||
self.bTypograf = False
|
self.bTypograf = False
|
||||||
if self.dtContentCreate is None:
|
if self.dtContentCreate is None:
|
||||||
self.dtContentCreate = datetime.datetime.now()
|
self.dtContentCreate = datetime.datetime.now()
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from unittest.mock import patch
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from etpgrf.config import MODE_UNICODE, SANITIZE_ETPGRF
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
from web.admin import AdminContentForm
|
||||||
from web.add_function import clean_text_to_slug, safe_html_special_symbols
|
from web.add_function import clean_text_to_slug, safe_html_special_symbols
|
||||||
from web.legacy_links import build_canonical_url, replace_legacy_links
|
from web.legacy_links import build_canonical_url, replace_legacy_links
|
||||||
from web.models import TbContent
|
from web.models import TbContent
|
||||||
@@ -50,6 +52,22 @@ class SafeHtmlSpecialSymbolsTests(SimpleTestCase):
|
|||||||
self.assertEqual(clean_text_to_slug('αβγ ΔΩ'), 'content')
|
self.assertEqual(clean_text_to_slug('αβγ ΔΩ'), 'content')
|
||||||
self.assertEqual(clean_text_to_slug('₽ € $ ₴ ₿'), 'content')
|
self.assertEqual(clean_text_to_slug('₽ € $ ₴ ₿'), 'content')
|
||||||
|
|
||||||
|
|
||||||
|
class AdminTypographFormTests(SimpleTestCase):
|
||||||
|
def test_admin_form_exposes_virtual_typograph_fields(self):
|
||||||
|
form = AdminContentForm()
|
||||||
|
|
||||||
|
self.assertNotIn('bTypograf', form.fields)
|
||||||
|
self.assertIn('typograph_enabled', form.fields)
|
||||||
|
self.assertIn('typograph_strip_soft_hyphens', form.fields)
|
||||||
|
self.assertIn('typograph_mode', form.fields)
|
||||||
|
self.assertIn('typograph_hyphenation', form.fields)
|
||||||
|
self.assertIn('typograph_sanitizer', form.fields)
|
||||||
|
self.assertEqual(form.fields['typograph_mode'].initial, 'mixed')
|
||||||
|
self.assertTrue(form.fields['typograph_strip_soft_hyphens'].initial)
|
||||||
|
self.assertTrue(form.fields['typograph_hyphenation'].initial)
|
||||||
|
self.assertEqual(form.fields['typograph_sanitizer'].initial, 'None')
|
||||||
|
|
||||||
def test_tbcontent_str_uses_clean_text(self):
|
def test_tbcontent_str_uses_clean_text(self):
|
||||||
item = TbContent(id=7, szContentHead='<b>«Привет мир»</b>')
|
item = TbContent(id=7, szContentHead='<b>«Привет мир»</b>')
|
||||||
|
|
||||||
@@ -129,17 +147,85 @@ class TypographTests(TestCase):
|
|||||||
bTypograf=True,
|
bTypograf=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch('web.models._TYPOGRAPHER_HEAD.process') as head_process_mock, \
|
with patch('web.models._build_typographer') as build_mock:
|
||||||
patch('web.models._TYPOGRAPHER_TEXT.process') as text_process_mock:
|
build_mock.return_value.process.side_effect = lambda text: f'[{text}]'
|
||||||
head_process_mock.side_effect = lambda text: f'HEAD[{text}]'
|
|
||||||
text_process_mock.side_effect = lambda text: f'TEXT[{text}]'
|
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
self.assertEqual(head_process_mock.call_count, 1)
|
self.assertEqual(build_mock.call_count, 2)
|
||||||
self.assertEqual(text_process_mock.call_count, 2)
|
self.assertEqual(item.szContentHead, '[«Привет»]')
|
||||||
self.assertEqual(item.szContentHead, 'HEAD[«Привет»]')
|
self.assertEqual(item.szContentIntro, '[<p>Абзац</p>]')
|
||||||
self.assertEqual(item.szContentIntro, 'TEXT[<p>Абзац</p>]')
|
self.assertEqual(item.szContentBody, '[<p>Тело</p>]')
|
||||||
self.assertEqual(item.szContentBody, 'TEXT[<p>Тело</p>]')
|
|
||||||
self.assertFalse(item.bTypograf)
|
self.assertFalse(item.bTypograf)
|
||||||
|
|
||||||
|
def test_save_uses_virtual_typograph_options(self):
|
||||||
|
item = TbContent(
|
||||||
|
szContentHead='Привет',
|
||||||
|
szContentIntro='Текст',
|
||||||
|
szContentBody='Тело',
|
||||||
|
bTypograf=True,
|
||||||
|
)
|
||||||
|
item._typograph_mode = MODE_UNICODE
|
||||||
|
item._typograph_hyphenation = False
|
||||||
|
item._typograph_sanitizer = SANITIZE_ETPGRF
|
||||||
|
|
||||||
|
with patch('web.models._build_typographer') as build_mock:
|
||||||
|
fake_typographer = build_mock.return_value
|
||||||
|
fake_typographer.process.side_effect = lambda text: text
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.assertEqual(build_mock.call_count, 2)
|
||||||
|
self.assertEqual(
|
||||||
|
build_mock.call_args_list[0].kwargs,
|
||||||
|
{
|
||||||
|
'mode': MODE_UNICODE,
|
||||||
|
'hyphenation': False,
|
||||||
|
'sanitizer': SANITIZE_ETPGRF,
|
||||||
|
'hanging_punctuation': 'left',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
build_mock.call_args_list[1].kwargs,
|
||||||
|
{
|
||||||
|
'mode': MODE_UNICODE,
|
||||||
|
'hyphenation': False,
|
||||||
|
'sanitizer': SANITIZE_ETPGRF,
|
||||||
|
'hanging_punctuation': False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_strips_soft_hyphens_before_typograph(self):
|
||||||
|
item = TbContent(
|
||||||
|
szContentHead='При­вет\u00ad',
|
||||||
|
szContentIntro='А­нонс',
|
||||||
|
szContentBody='Те­ло\u00ad',
|
||||||
|
bTypograf=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch('web.models._build_typographer') as build_mock:
|
||||||
|
build_mock.return_value.process.side_effect = lambda text: f'[{text}]'
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.assertEqual(build_mock.call_count, 2)
|
||||||
|
self.assertEqual(item.szContentHead, '[Привет]')
|
||||||
|
self.assertEqual(item.szContentIntro, '[Анонс]')
|
||||||
|
self.assertEqual(item.szContentBody, '[Тело]')
|
||||||
|
self.assertFalse(item.bTypograf)
|
||||||
|
|
||||||
|
def test_show_item_increments_hits_without_touching_timestamp(self):
|
||||||
|
item = TbContent.objects.create(
|
||||||
|
szContentHead='Проверка просмотра',
|
||||||
|
szContentIntro='Короткий анонс',
|
||||||
|
szContentBody='Полный текст',
|
||||||
|
szContentSlug='proverka-prosmotra',
|
||||||
|
bContentPublish=True,
|
||||||
|
)
|
||||||
|
timestamp_before = item.dtContentTimeStamp
|
||||||
|
|
||||||
|
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.iContentHits, 1)
|
||||||
|
self.assertEqual(item.dtContentTimeStamp, timestamp_before)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user