minor: валидатор форм, парсера и моделей (25) админка OfferAdmin и формы OfferAdminForm для модели TbOffer "одеты" с CodeMirror
This commit is contained in:
+142
-15
@@ -947,13 +947,13 @@ class ArticleAdminForm(CodeMirrorFormMixin):
|
|||||||
label="Кавычки",
|
label="Кавычки",
|
||||||
initial=True,
|
initial=True,
|
||||||
required=False,
|
required=False,
|
||||||
help_text="Заменять кавычки</br>(«ёлочки» для русского, “лапки” для английского) "
|
help_text="Заменять кавычки<br/>(«ёлочки» для русского, “лапки” для английского) "
|
||||||
)
|
)
|
||||||
etp_hyphenation = forms.BooleanField(
|
etp_hyphenation = forms.BooleanField(
|
||||||
label="Расставлять переносы",
|
label="Расставлять переносы",
|
||||||
initial=True,
|
initial=True,
|
||||||
required=False,
|
required=False,
|
||||||
help_text="Расставлять мягкие переносы (&shy;) </br>"
|
help_text="Расставлять мягкие переносы (&shy;) <br/>"
|
||||||
"в словах длиннее <b>14</b> символов "
|
"в словах длиннее <b>14</b> символов "
|
||||||
)
|
)
|
||||||
etp_sanitize = forms.BooleanField(
|
etp_sanitize = forms.BooleanField(
|
||||||
@@ -966,7 +966,7 @@ class ArticleAdminForm(CodeMirrorFormMixin):
|
|||||||
label="Висячая пунктуация",
|
label="Висячая пунктуация",
|
||||||
initial=False,
|
initial=False,
|
||||||
required=False,
|
required=False,
|
||||||
help_text="Выносить пунктуацию в начало строк</br> "
|
help_text="Выносить пунктуацию в начало строк<br/> "
|
||||||
"(только для заголовков... для тизера и контента отключается автоматически) "
|
"(только для заголовков... для тизера и контента отключается автоматически) "
|
||||||
)
|
)
|
||||||
etp_mode = forms.ChoiceField(
|
etp_mode = forms.ChoiceField(
|
||||||
@@ -1045,14 +1045,14 @@ class ArticleAdminForm(CodeMirrorFormMixin):
|
|||||||
# ...предлагаем изменить slug
|
# ...предлагаем изменить slug
|
||||||
error_html = (
|
error_html = (
|
||||||
'<div class="confirmation-button-container">'
|
'<div class="confirmation-button-container">'
|
||||||
' <big>Ой! Кажется, вы изменили заголовок статьи!</big></br>'
|
' <big>Ой! Кажется, вы изменили заголовок статьи!</big><br/>'
|
||||||
f' Текущий slug: <b><tt><u>{self.instance.slug}</u></tt></b></br>'
|
f' Текущий slug: <b><tt><u>{self.instance.slug}</u></tt></b><br/>'
|
||||||
f' Для измененного заголовка «<tt><i><u>{cleaned_data.get('s_article_title_html')}</u></i></tt>»'
|
f' Для измененного заголовка «<tt><i><u>{cleaned_data.get('s_article_title_html')}</u></i></tt>»'
|
||||||
f' лучше сделать slug <b><tt><u>{new_potential_slug}</u></tt></b></br></br>'
|
f' лучше сделать slug <b><tt><u>{new_potential_slug}</u></tt></b><br/><br/>'
|
||||||
' Пожалуйста, проверьте, что это правильно. Если вы уверены — нажмите подтверждение.<br></br>'
|
' Пожалуйста, проверьте, что это правильно. Если вы уверены — нажмите подтверждение.<br><br/>'
|
||||||
' <button type="button" onclick="markSubmitButtonsToIgnoreValidation();">'
|
' <button type="button" onclick="markSubmitButtonsToIgnoreValidation();">'
|
||||||
' ✓ Я ПРОВЕРИЛ И УВЕРЕН!'
|
' ✓ Я ПРОВЕРИЛ И УВЕРЕН!'
|
||||||
' </button></br> '
|
' </button><br/> '
|
||||||
'</div>'
|
'</div>'
|
||||||
)
|
)
|
||||||
raise ValidationError(mark_safe(error_html))
|
raise ValidationError(mark_safe(error_html))
|
||||||
@@ -1193,13 +1193,92 @@ class ArticleAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# АДМИНКА КОММЕРЧЕСКИХ ПРЕДЛОЖЕНИЙ (ОФФЕРОВ)
|
||||||
|
#
|
||||||
|
# Статьи очень важная сущность сайта. Через них на сайте отпределяютя URL (slag), SEO-поля и "заголовочные" картинки
|
||||||
|
# многих сущностей (артистов, лейблов, музыкальных стилей и т.д.). Также тут опредлелентся, как на сайте будут
|
||||||
|
# отображаться все эти сущности сверстанными в HTML.
|
||||||
|
|
||||||
|
# Кастомная форма
|
||||||
|
class OfferAdminForm(CodeMirrorFormMixin):
|
||||||
|
"""
|
||||||
|
Кастомная форма для админки коммерческих предложений (TbOffer).
|
||||||
|
Добавляет виджеты CodeMirror для текстовых полей
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = TbOffer
|
||||||
|
fields = (
|
||||||
|
's_offer',
|
||||||
|
'l_offer_to_format',
|
||||||
|
'k_offer_to_article',
|
||||||
|
'k_offer_to_item',
|
||||||
|
'k_offer_to_label',
|
||||||
|
'k_offer_to_source',
|
||||||
|
's_offer_catalog_num',
|
||||||
|
'b_offer_is_preorder',
|
||||||
|
'd_offer_date_release',
|
||||||
|
'i_offer_discogs_id',
|
||||||
|
'l_offer_condition_media',
|
||||||
|
'l_offer_condition_sleeve',
|
||||||
|
'f_offer_price',
|
||||||
|
'i_offer_quantity',
|
||||||
|
'i_offer_discount_to_daily_sale',
|
||||||
|
'j_offer_metadata',
|
||||||
|
'i_offer_views',
|
||||||
|
'i_offer_favorites',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
При инициализации формы подгружаем CodeMirror редактор.
|
||||||
|
Получаем request из kwargs, переданных из get_form_kwargs в AdminClass.
|
||||||
|
"""
|
||||||
|
# Извлекаем request из kwargs если он есть
|
||||||
|
self.request = kwargs.pop('request', None)
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Конфигурируем поля для CodeMirror
|
||||||
|
self.setup_codemirror_field('s_offer', language='text',
|
||||||
|
css_class='codemirror-width-l codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('f_offer_price', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('i_offer_quantity', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('j_offer_metadata', language='json',
|
||||||
|
css_class='codemirror-width-l codemirror-min-height-5')
|
||||||
|
self.setup_codemirror_field('i_offer_views', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('i_offer_favorites', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('s_offer_catalog_num', language='text',
|
||||||
|
css_class='codemirror-width-m codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('i_offer_discogs_id', language='text',
|
||||||
|
css_class='codemirror-width-m codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('i_offer_discount_to_daily_sale', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Остальные ModelAdmin классы
|
# АДМИНКА ОФФЕРОВ (OfferAdminForm, OfferImageInlineForm, OfferAdmin)
|
||||||
# Их еще предстоит обжужукать (натянуть CodeMirror, обвесить валидаторами, переопределить save_model и т.д.)
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
class OfferAdmin(admin.ModelAdmin):
|
|
||||||
"""Админ для предложений"""
|
class OfferImageInlineForm(CodeMirrorFormMixin):
|
||||||
|
"""Форма для inline картинок с CodeMirror стилями"""
|
||||||
|
class Meta:
|
||||||
|
model = TbImageMetadata
|
||||||
|
fields = ('image', 'i_img_sort')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Стилизуем i_img_sort в стиле CodeMirror (как число, компактное)
|
||||||
|
self.setup_codemirror_field('i_img_sort', language='text',
|
||||||
|
css_class='codemirror-width-s codemirror-no-lines')
|
||||||
|
|
||||||
|
class OfferAdmin(RequestInFormMixin, admin.ModelAdmin):
|
||||||
|
"""Админ для предложений с поддержкой передачи request в форму"""
|
||||||
|
form = OfferAdminForm # Используем кастомную форму с CodeMirror
|
||||||
|
|
||||||
|
|
||||||
class OfferImageInline(admin.TabularInline):
|
class OfferImageInline(admin.TabularInline):
|
||||||
"""
|
"""
|
||||||
@@ -1208,21 +1287,69 @@ class OfferAdmin(admin.ModelAdmin):
|
|||||||
Позволяет добавлять, редактировать и удалять картинки через:
|
Позволяет добавлять, редактировать и удалять картинки через:
|
||||||
- FilerImageField для красивого выбора
|
- FilerImageField для красивого выбора
|
||||||
- i_img_sort для сортировки
|
- i_img_sort для сортировки
|
||||||
- j_img_metadata для метаданных
|
|
||||||
"""
|
"""
|
||||||
model = TbImageMetadata
|
model = TbImageMetadata
|
||||||
|
form = OfferImageInlineForm
|
||||||
fk_name = 'm_offer'
|
fk_name = 'm_offer'
|
||||||
extra = 1 # одна пустая строка для добавления новой картинки
|
extra = 1 # одна пустая строка для добавления новой картинки
|
||||||
fields = ('image', 'i_img_sort', 'j_img_metadata')
|
fields = ('image', 'i_img_sort')
|
||||||
ordering = ('i_img_sort',)
|
ordering = ('i_img_sort',)
|
||||||
|
verbose_name = 'Изображения и метаданные-изображений коммерческого предложения'
|
||||||
|
verbose_name_plural = 'Изображения и метаданные-изображений коммерческих предложений'
|
||||||
|
|
||||||
list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views')
|
list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views')
|
||||||
|
list_display_links = ('id', 's_offer',)
|
||||||
list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format')
|
list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format')
|
||||||
search_fields = ('s_offer',)
|
search_fields = ('s_offer',)
|
||||||
inlines = [OfferImageInline] # Управление картинками в inline
|
inlines = [OfferImageInline] # Управление картинками в inline
|
||||||
readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated', 'i_offer_views', 'i_offer_favorites')
|
readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated',)
|
||||||
|
fieldsets = (
|
||||||
|
('Код товара в базе', {
|
||||||
|
'fields': ('s_offer_skip32',),
|
||||||
|
}),
|
||||||
|
('Основная информация', {
|
||||||
|
'fields': ('k_offer_to_item', 's_offer', 'f_offer_price', 'i_offer_quantity'),
|
||||||
|
}),
|
||||||
|
('Дата релиза и метка если доступно только по предзаказу', {
|
||||||
|
'fields': ('d_offer_date_release', 'b_offer_is_preorder',),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
('Дополнительные данные', {
|
||||||
|
'fields': (
|
||||||
|
'l_offer_to_format',
|
||||||
|
'l_offer_condition_media',
|
||||||
|
'l_offer_condition_sleeve',
|
||||||
|
'k_offer_to_article',
|
||||||
|
'k_offer_to_label',
|
||||||
|
'k_offer_to_source',
|
||||||
|
's_offer_catalog_num',
|
||||||
|
'i_offer_discogs_id',
|
||||||
|
|
||||||
|
'i_offer_discount_to_daily_sale',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
('Метаданные коммерческого предложения', {
|
||||||
|
'fields': ('j_offer_metadata',),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
('Просмотры и избранное', {
|
||||||
|
'fields': ('i_offer_views', 'i_offer_favorites',),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
('Служебная информация', {
|
||||||
|
'fields': ('t_offer_created', 't_offer_updated'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Остальные ModelAdmin классы
|
||||||
|
# Их еще предстоит обжужукать (натянуть CodeMirror, обвесить валидаторами, переопределить save_model и т.д.)
|
||||||
|
# ============================================================================
|
||||||
class OfferHistoryAdmin(admin.ModelAdmin):
|
class OfferHistoryAdmin(admin.ModelAdmin):
|
||||||
"""Админ для истории изменений офферов"""
|
"""Админ для истории изменений офферов"""
|
||||||
list_display = ('id', 'k_history_to_offer', 'f_history_price', 'i_history_quantity', 't_history_created')
|
list_display = ('id', 'k_history_to_offer', 'f_history_price', 'i_history_quantity', 't_history_created')
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ from lpon_site.settings import (
|
|||||||
VALUE_IMAGE_FROM_USER, # Картинка загружена пользователем через админку
|
VALUE_IMAGE_FROM_USER, # Картинка загружена пользователем через админку
|
||||||
KEY_IMAGE_URL, # Ключ, о том, откуда получена картинка (URL источника)
|
KEY_IMAGE_URL, # Ключ, о том, откуда получена картинка (URL источника)
|
||||||
KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.)
|
KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.)
|
||||||
|
KEY_OFFER_NOTE, # Ключ, для заметок о коммерческом предложении (например, "замят угол конверта" и т.д.)
|
||||||
|
KEY_OFFER_ALL_MEDIA, # Ключ, для хранения информации о том, какие носители входят в оффер (например, CD+2LP)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -414,7 +416,7 @@ class TbArticle(models.Model):
|
|||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name='Изображение для статьи',
|
verbose_name='Изображение для статьи',
|
||||||
help_text='Обложка или иллюстрация статьи из файлового хранилища (django_filer).'
|
help_text='Обложка или иллюстрация статьи из файлового хранилища (django_filer).'
|
||||||
' Метаданные: image.metadata.i_img_sort и image.metadata.j_img_metadata.</br>'
|
' Метаданные: image.metadata.i_img_sort и image.metadata.j_img_metadata.<br/>'
|
||||||
'<b>ВАЖНО</b>: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям,'
|
'<b>ВАЖНО</b>: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям,'
|
||||||
' её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае '
|
' её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае '
|
||||||
' рекомендуется использовать изображение с прозрачным фоном (движок поддерживает SVG, WebP и PNG).'
|
' рекомендуется использовать изображение с прозрачным фоном (движок поддерживает SVG, WebP и PNG).'
|
||||||
@@ -1151,7 +1153,7 @@ class TbOffer(models.Model):
|
|||||||
verbose_name='Формат',
|
verbose_name='Формат',
|
||||||
help_text='Форматы основного носителей (пластинка, CD, кассета и т.п.). Если несколько носителей (и разных),'
|
help_text='Форматы основного носителей (пластинка, CD, кассета и т.п.). Если несколько носителей (и разных),'
|
||||||
' то это указывать в "Дополнительных данных" в JSON-формате. Например:'
|
' то это указывать в "Дополнительных данных" в JSON-формате. Например:'
|
||||||
' <tt>{"formats": {"lp": 2, "cd": 1},}</tt>',
|
' <tt>{"OFR_MF": {"lp": 2, "cd": 1},}</tt>',
|
||||||
)
|
)
|
||||||
# Связи
|
# Связи
|
||||||
k_offer_to_article = models.ForeignKey(
|
k_offer_to_article = models.ForeignKey(
|
||||||
@@ -1179,13 +1181,15 @@ class TbOffer(models.Model):
|
|||||||
)
|
)
|
||||||
k_offer_to_label = models.ForeignKey(
|
k_offer_to_label = models.ForeignKey(
|
||||||
TbLabel,
|
TbLabel,
|
||||||
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='label_to_offer', # ← label.label_to_offers.all()
|
related_name='label_to_offer', # ← label.label_to_offers.all()
|
||||||
db_index=True, # Принудительно создаем индекс, т.к. SQLite их сам не создаст.
|
db_index=True, # Принудительно создаем индекс, т.к. SQLite их сам не создаст.
|
||||||
verbose_name='Лейбл',
|
verbose_name='Лейбл/Производитель',
|
||||||
help_text='Лейбл, на котором был выпущен релиз, если он известен. Например: <tt>Atlantic</tt> или <tt>Мелодия</tt>',
|
help_text='Лейбл, на котором был выпущен релиз (если известен), или производитель для аудиио-кассет и MD'
|
||||||
|
' под запись, технику или аксессуар.',
|
||||||
)
|
)
|
||||||
k_offer_to_source = models.ForeignKey(
|
k_offer_to_source = models.ForeignKey(
|
||||||
to='TbSource',
|
to='TbSource',
|
||||||
@@ -1226,7 +1230,7 @@ class TbOffer(models.Model):
|
|||||||
help_text='Если товар доступен только для предзаказа',
|
help_text='Если товар доступен только для предзаказа',
|
||||||
)
|
)
|
||||||
d_offer_date_release = models.DateField(
|
d_offer_date_release = models.DateField(
|
||||||
blank=False,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
@@ -1278,9 +1282,26 @@ class TbOffer(models.Model):
|
|||||||
)
|
)
|
||||||
j_offer_metadata = models.JSONField(
|
j_offer_metadata = models.JSONField(
|
||||||
# Метаданные оффера (сырые данные из источника, координаты в Excel и т.д.)
|
# Метаданные оффера (сырые данные из источника, координаты в Excel и т.д.)
|
||||||
default=dict, null=True,
|
default={
|
||||||
|
KEY_OFFER_NOTE: '',
|
||||||
|
KEY_OFFER_ALL_MEDIA: [],
|
||||||
|
},
|
||||||
|
null=True,
|
||||||
verbose_name='Дополнительные данные',
|
verbose_name='Дополнительные данные',
|
||||||
help_text='Дополнительные данные о предложении в виде JSON-словаря.',
|
help_text='Дополнительные данные о предложении в виде JSON-словаря. Например:<pre style=\"'
|
||||||
|
'background-color:#80808080;border:1px solid #ccc; padding:1ex;max-width: calc(75% - 13em);\">{\n'
|
||||||
|
f' \"{KEY_OFFER_NOTE}\": \"Уценка: замят угол конверта, повреждены наклейки.\",\n'
|
||||||
|
f' \"{KEY_OFFER_ALL_MEDIA}\": '
|
||||||
|
'{\n "lp": 2,\n "cd": 1\n }\n}</pre>'
|
||||||
|
f'Ключи:<ul>'
|
||||||
|
f' <li>\"{KEY_OFFER_NOTE}\" — Примечание к офферу</li>'
|
||||||
|
f' <li>\"{KEY_OFFER_ALL_MEDIA}\" — Какие носители входят в коммерческое предложение.<br/>'
|
||||||
|
' Допустимые значения: \"lp\" — Vinyl Long-Play (12"), \"ep\" — Vinyl Extended-Play (12\", 10\",'
|
||||||
|
' 7\"), \"45\" — Vinyl 7\" (45 rpm), \"cd\" — Compact Disc, \"ld\" — LaserDisc,'
|
||||||
|
' \"md\" — MiniDisc Record, \"ms\" — Used MiniDisc (для записи),'
|
||||||
|
' \"cs\" — Cassette Record, \"uc\" — Used Cassette (для записи),'
|
||||||
|
' \"tp\" — Tape Reel Record, \"ur\" — Used Tape Reel (для записи),'
|
||||||
|
' \"??\" — Other</li></ul>',
|
||||||
)
|
)
|
||||||
s_offer_skip32 = models.CharField(
|
s_offer_skip32 = models.CharField(
|
||||||
max_length=12,
|
max_length=12,
|
||||||
|
|||||||
@@ -330,6 +330,10 @@ VALUE_IMAGE_FROM_PARSE = 'parse' # Картинка получена через
|
|||||||
KEY_IMAGE_URL = 'IMG_URL' # Ключ для хранения URL оригинальной картинки (обычно это должен прописать парсер)
|
KEY_IMAGE_URL = 'IMG_URL' # Ключ для хранения URL оригинальной картинки (обычно это должен прописать парсер)
|
||||||
KEY_IMAGE_NOTE = 'IMG_NOTE' # Ключ для хранения заметки о картинке
|
KEY_IMAGE_NOTE = 'IMG_NOTE' # Ключ для хранения заметки о картинке
|
||||||
|
|
||||||
|
# Ключи и значения для коммерческих предложений (офферов, см. TbOffer и OfferAdmin)
|
||||||
|
KEY_OFFER_NOTE = 'OFR_NT' # Ключ для заметок по офферу (например, "замят угол конверта" и т.д.)
|
||||||
|
KEY_OFFER_ALL_MEDIA = 'OFR_MF' # Ключ для хранения информации о том, какие носители входят в оффер (например, CD+2LP)
|
||||||
|
|
||||||
|
|
||||||
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
||||||
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
||||||
|
|||||||
Reference in New Issue
Block a user