UI - Fixing notification settings still trying to access main settings form that caused a crash on save (#4561)

This commit is contained in:
dgtlmoon authored and GitHub committed 2026-10-16 11:05:23 +02:00
1 parent c09bb66e33
commit c58d12245e
23 files changed
+231 -186

No files matched your search

@@ -66,9 +66,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
extra_notification_tokens=datastore.get_unique_notification_tokens_available()
)
# Remove the last option 'System default'
form.application.form.notification_format.choices.pop()
if datastore.proxy_list is None:
# @todo - Couldn't get setattr() etc dynamic addition working, so remove it instead
del form.requests.form.proxy
@@ -95,10 +92,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
if 'password' in app_update and not app_update['password']:
del (app_update['password'])
# Notification fields live on the dedicated /settings/notifications page now.
# The application sub-form still defines them (inherited from commonSettingsForm),
# so an unrelated save here would clobber the stored values with empty WTForms
# defaults. Drop them from the merge to leave the notifications config alone.
# Notification fields live on the dedicated /settings/notifications page and are
# no longer part of this form (#4554). Defensive - never let a save here clobber
# the stored notification config should one get re-added to the application sub-form.
for nf in ('notification_urls', 'notification_title', 'notification_body',
'notification_format', 'base_url'):
app_update.pop(nf, None)
+29 -32
View File
@@ -51,6 +51,7 @@ from changedetectionio.browser_steps.browser_steps import browser_step_ui_config
from changedetectionio import html_tools, content_fetchers
from changedetectionio.notification import (
USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH,
valid_notification_formats,
)
@@ -860,21 +861,37 @@ class quickWatchForm(Form):
edit_and_watch_submit_button = SubmitField(_l('Edit > Watch'), render_kw={"class": "pure-button pure-button-primary"})
# Apprise notification fields, common to a single watch and the global
# /settings/notifications page (NOT the main /settings page, which no longer
# renders them - a hidden field there can fail validation with no visible error, see #4554)
class commonNotificationSettingsForm(Form):
def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
super().__init__(formdata, obj, prefix, data, meta, **kwargs)
extra = kwargs.get('extra_notification_tokens', {})
self.notification_body.extra_notification_tokens = extra
self.notification_title.extra_notification_tokens = extra
self.notification_urls.extra_notification_tokens = extra
notification_urls = StringListField(_l('Notification URL List'),
validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()])
notification_title = StringField(_l('Notification Title'),
default='ChangeDetection.io Notification - {{ watch_url }}',
validators=[validators.Optional(), ValidateJinja2Template()])
notification_body = TextAreaField(_l('Notification Body'),
default='{{ watch_url }} had a change.',
validators=[validators.Optional(), ValidateJinja2Template()])
notification_format = SelectField(_l('Notification format'),
choices=list(valid_notification_formats.items()))
# Common to a single watch and the global settings
class commonSettingsForm(Form):
from . import processors
def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
super().__init__(formdata, obj, prefix, data, meta, **kwargs)
self.notification_body.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
self.notification_title.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
self.notification_urls.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
fetch_backend = RadioField(_l('Fetch Method'), choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
notification_body = TextAreaField(_l('Notification Body'), default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()])
notification_format = SelectField(_l('Notification format'), choices=list(valid_notification_formats.items()))
notification_title = StringField(_l('Notification Title'), default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()])
notification_urls = StringListField(_l('Notification URL List'), validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()])
processor = RadioField( label=_l("Processor - What do you want to achieve?"), choices=lambda: processors.available_processors(), default=processors.get_default_processor)
scheduler_timezone_default = StringField(_l("Default timezone for watch check scheduler"), render_kw={"list": "timezones"}, validators=[validateTimeZoneName()])
webdriver_delay = IntegerField(_l('Wait seconds before extracting text'), validators=[validators.Optional(), validators.NumberRange(min=1, message=_l("Should contain one or more seconds"))])
@@ -900,24 +917,11 @@ class commonSettingsForm(Form):
#
# Named for the backend (apprise) on purpose: future backends (simple_email,
# webhook, etc.) will land as their own forms next to this one.
class globalSettingsAppriseNotificationForm(Form):
def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
super().__init__(formdata, obj, prefix, data, meta, **kwargs)
extra = kwargs.get('extra_notification_tokens', {})
self.notification_body.extra_notification_tokens = extra
self.notification_title.extra_notification_tokens = extra
self.notification_urls.extra_notification_tokens = extra
notification_urls = StringListField(_l('Notification URL List'),
validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()])
notification_title = StringField(_l('Notification Title'),
default='ChangeDetection.io Notification - {{ watch_url }}',
validators=[validators.Optional(), ValidateJinja2Template()])
notification_body = TextAreaField(_l('Notification Body'),
default='{{ watch_url }} had a change.',
validators=[validators.Optional(), ValidateJinja2Template()])
class globalSettingsAppriseNotificationForm(commonNotificationSettingsForm):
# 'System default' only makes sense per-watch (it means "use this global value")
notification_format = SelectField(_l('Notification format'),
choices=list(valid_notification_formats.items()))
choices=[(k, v) for k, v in valid_notification_formats.items()
if k != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH])
base_url = StringField(_l('Notification base URL override'),
validators=[validators.Optional()],
render_kw={"placeholder": os.getenv('BASE_URL', _l('Not set'))})
@@ -941,7 +945,7 @@ class SingleBrowserStep(Form):
# remove_button = SubmitField(_l('-'), render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Remove'})
# add_button = SubmitField(_l('+'), render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Add new step after'})
class processor_text_json_diff_form(commonSettingsForm):
class processor_text_json_diff_form(commonSettingsForm, commonNotificationSettingsForm):
url = StringField(_l('Web Page URL'), validators=[validateURL()])
link_to_open = StringField(_l('Open Link Override'), validators=[validators.Optional(), validateURL()], default='')
@@ -1192,10 +1196,6 @@ class globalSettingsApplicationUIForm(Form):
class globalSettingsApplicationForm(commonSettingsForm):
api_access_token_enabled = BooleanField(_l('API access token security check enabled'), default=True, validators=[validators.Optional()])
base_url = StringField(_l('Notification base URL override'),
validators=[validators.Optional()],
render_kw={"placeholder": os.getenv('BASE_URL', _l('Not set'))}
)
empty_pages_are_a_change = BooleanField(_l('Treat empty pages as a change?'), default=False)
fetch_backend = RadioField(_l('Fetch Method'), default="html_requests", choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
global_ignore_text = StringListField(_l('Ignore Text'), [ValidateListRegex()])
@@ -1417,9 +1417,6 @@ class globalSettingsForm(Form):
# datastore.data['settings']['requests']..
def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
super().__init__(formdata, obj, prefix, data, meta, **kwargs)
self.application.notification_body.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
self.application.notification_title.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
self.application.notification_urls.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
requests = FormField(globalSettingsRequestForm)
application = FormField(globalSettingsApplicationForm)
@@ -118,10 +118,8 @@ def test_main_settings_save_does_not_clobber_notification_config(
"""
Regression — the main /settings page and the new /settings/notifications page
write to the SAME dict (datastore.data['settings']['application']). The main
page's application sub-form still inherits the notification_* fields from
commonSettingsForm, so a naive `app.update(form.data['application'])` here
would wipe notification_urls / title / body / format / base_url with empty
WTForms defaults whenever the user saves the main settings page.
page must never write notification_urls / title / body / format / base_url,
otherwise saving it would wipe them with empty WTForms defaults.
This test seeds a real notification config, then POSTs the main settings
page WITHOUT touching the notification fields, and asserts every notification
@@ -139,7 +137,6 @@ def test_main_settings_save_does_not_clobber_notification_config(
'application-fetch_backend': 'html_requests',
'application-rss_diff_length': '5',
'application-filter_failure_notification_threshold_attempts': '0',
'application-notification_format': 'html', # required choice, no Optional()
'requests-time_between_check-days': '0',
'requests-time_between_check-hours': '0',
'requests-time_between_check-minutes': '5',
@@ -225,3 +222,58 @@ def test_notifications_page_save_does_not_clobber_other_settings(
# And the notification field we DID submit was actually written.
assert ds.data['settings']['application'].get('notification_urls') == \
['json://example.invalid/only-this']
def test_main_settings_save_with_global_system_default_format(
client, live_server, measure_memory_usage, datastore_path):
"""
Regression for #4554 - a global notification_format of 'System default' made
every save of the main /settings page fail with "An error occurred" and no
visible field error, because the main form still carried a hidden
notification_format SelectField whose choices excluded that value.
"""
from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
ds = client.application.config.get('DATASTORE')
ds.data['settings']['application']['notification_format'] = USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
res = client.post(
url_for('settings.settings_page'),
data={
'application-pager_size': '50',
'application-fetch_backend': 'html_requests',
'application-rss_diff_length': '5',
'application-filter_failure_notification_threshold_attempts': '0',
'requests-time_between_check-days': '0',
'requests-time_between_check-hours': '0',
'requests-time_between_check-minutes': '5',
'requests-time_between_check-seconds': '0',
'requests-time_between_check-weeks': '0',
'requests-jitter_seconds': '0',
'requests-workers': '10',
'requests-timeout': '60',
},
follow_redirects=True,
)
assert res.status_code == 200
assert b'Settings updated.' in res.data
assert b'An error occurred' not in res.data
def test_notifications_page_rejects_system_default_format(
client, live_server, measure_memory_usage, datastore_path):
"""'System default' is a per-watch choice meaning "use the global value",
it must not be offered or accepted as the global value itself (#4554)."""
from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
res = client.get(url_for('settings.notifications.apprise'))
assert f'value="{USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH}"'.encode() not in res.data
res = client.post(
url_for('settings.notifications.apprise'),
data={'notification_format': USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH},
follow_redirects=True,
)
assert b'Settings updated.' not in res.data
ds = client.application.config.get('DATASTORE')
assert ds.data['settings']['application'].get('notification_format') != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
@@ -3229,8 +3229,12 @@ msgid "Edit > Watch"
msgstr "Upravit > Monitorovat"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Nastavte metodu načítání"
msgid "Notification URL List"
msgstr "Seznam URL oznámení"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Název oznámení"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3241,12 +3245,8 @@ msgid "Notification format"
msgstr "Formát oznámení"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Název oznámení"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Seznam URL oznámení"
msgid "Fetch Method"
msgstr "Nastavte metodu načítání"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3278,8 +3278,12 @@ msgid "Edit > Watch"
msgstr "Bearbeiten"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Fetch Methode"
msgid "Notification URL List"
msgstr "Liste der Benachrichtigungs-URLs"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Benachrichtigungstitel"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3290,12 +3294,8 @@ msgid "Notification format"
msgstr "Benachrichtigungsformat"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Benachrichtigungstitel"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Liste der Benachrichtigungs-URLs"
msgid "Fetch Method"
msgstr "Fetch Methode"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3221,7 +3221,11 @@ msgid "Edit > Watch"
msgstr ""
#: changedetectionio/forms.py
msgid "Fetch Method"
msgid "Notification URL List"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
@@ -3233,11 +3237,7 @@ msgid "Notification format"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification URL List"
msgid "Fetch Method"
msgstr ""
#: changedetectionio/forms.py
@@ -3221,7 +3221,11 @@ msgid "Edit > Watch"
msgstr ""
#: changedetectionio/forms.py
msgid "Fetch Method"
msgid "Notification URL List"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
@@ -3233,11 +3237,7 @@ msgid "Notification format"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification URL List"
msgid "Fetch Method"
msgstr ""
#: changedetectionio/forms.py
@@ -3304,8 +3304,12 @@ msgid "Edit > Watch"
msgstr "Editar > Ver"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Método de recuperación"
msgid "Notification URL List"
msgstr "Lista de URL de notificación"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Título de la notificación"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3316,12 +3320,8 @@ msgid "Notification format"
msgstr "Formato de notificación"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Título de la notificación"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Lista de URL de notificación"
msgid "Fetch Method"
msgstr "Método de recuperación"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3234,8 +3234,12 @@ msgid "Edit > Watch"
msgstr "Modifier > Surveiller"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Définir la méthode de récupération"
msgid "Notification URL List"
msgstr "Liste d'URL de notification"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Titre de notification"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3246,12 +3250,8 @@ msgid "Notification format"
msgstr "Format de notification"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Titre de notification"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Liste d'URL de notification"
msgid "Fetch Method"
msgstr "Définir la méthode de récupération"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3334,8 +3334,12 @@ msgid "Edit > Watch"
msgstr "Edit > Monitor"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Metode Pengambilan"
msgid "Notification URL List"
msgstr "Daftar URL Notifikasi"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Judul Notifikasi"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3346,12 +3350,8 @@ msgid "Notification format"
msgstr "Format notifikasi"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Judul Notifikasi"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Daftar URL Notifikasi"
msgid "Fetch Method"
msgstr "Metode Pengambilan"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3223,8 +3223,12 @@ msgid "Edit > Watch"
msgstr "Modifica > Monitora"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Metodo di recupero"
msgid "Notification URL List"
msgstr "Lista URL notifiche"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Titolo notifica"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3235,12 +3239,8 @@ msgid "Notification format"
msgstr "Formato notifica"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Titolo notifica"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Lista URL notifiche"
msgid "Fetch Method"
msgstr "Metodo di recupero"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3240,8 +3240,12 @@ msgid "Edit > Watch"
msgstr "編集 > ウォッチ"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "取得方式"
msgid "Notification URL List"
msgstr "通知URLリスト"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知タイトル"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3252,12 +3256,8 @@ msgid "Notification format"
msgstr "通知フォーマット"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知タイトル"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "通知URLリスト"
msgid "Fetch Method"
msgstr "取得方式"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3231,8 +3231,12 @@ msgid "Edit > Watch"
msgstr "편집 > 모니터링"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "가져오기 방식"
msgid "Notification URL List"
msgstr "알림 URL 목록"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "알림 제목"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3243,12 +3247,8 @@ msgid "Notification format"
msgstr "알림 형식"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "알림 제목"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "알림 URL 목록"
msgid "Fetch Method"
msgstr "가져오기 방식"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
+7 -7
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.60.9\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-10-13 11:16+0200\n"
"POT-Creation-Date: 2026-10-16 10:41+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -3220,7 +3220,11 @@ msgid "Edit > Watch"
msgstr ""
#: changedetectionio/forms.py
msgid "Fetch Method"
msgid "Notification URL List"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
@@ -3232,11 +3236,7 @@ msgid "Notification format"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr ""
#: changedetectionio/forms.py
msgid "Notification URL List"
msgid "Fetch Method"
msgstr ""
#: changedetectionio/forms.py
@@ -3380,8 +3380,12 @@ msgid "Edit > Watch"
msgstr "Edycja > Obserwuj"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Metoda pobierania"
msgid "Notification URL List"
msgstr "Lista adresów URL powiadomień"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Tytuł powiadomienia"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3392,12 +3396,8 @@ msgid "Notification format"
msgstr "Format powiadomienia"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Tytuł powiadomienia"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Lista adresów URL powiadomień"
msgid "Fetch Method"
msgstr "Metoda pobierania"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3271,8 +3271,12 @@ msgid "Edit > Watch"
msgstr "Editar > Monitoramento"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Método de Busca"
msgid "Notification URL List"
msgstr "Lista de URLs de Notificação"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Título da Notificação"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3283,12 +3287,8 @@ msgid "Notification format"
msgstr "Formato da notificação"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Título da Notificação"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Lista de URLs de Notificação"
msgid "Fetch Method"
msgstr "Método de Busca"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3339,8 +3339,12 @@ msgid "Edit > Watch"
msgstr "Редактировать > Смотреть"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Метод выборки"
msgid "Notification URL List"
msgstr "Список URL-адресов уведомлений"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Название уведомления"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3351,12 +3355,8 @@ msgid "Notification format"
msgstr "Формат уведомления"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Название уведомления"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Список URL-адресов уведомлений"
msgid "Fetch Method"
msgstr "Метод выборки"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3274,8 +3274,12 @@ msgid "Edit > Watch"
msgstr "Düzenle > İzleyici"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Getirme Yöntemi"
msgid "Notification URL List"
msgstr "Bildirim URL Listesi"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Bildirim Başlığı"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3286,12 +3290,8 @@ msgid "Notification format"
msgstr "Bildirim formatı"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Bildirim Başlığı"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Bildirim URL Listesi"
msgid "Fetch Method"
msgstr "Getirme Yöntemi"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3253,8 +3253,12 @@ msgid "Edit > Watch"
msgstr "Редагувати > Завдання"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "Метод завантаження"
msgid "Notification URL List"
msgstr "Список URL сповіщень"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Заголовок сповіщення"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3265,12 +3269,8 @@ msgid "Notification format"
msgstr "Формат сповіщення"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "Заголовок сповіщення"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "Список URL сповіщень"
msgid "Fetch Method"
msgstr "Метод завантаження"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3227,8 +3227,12 @@ msgid "Edit > Watch"
msgstr "编辑 > 监控项"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "抓取方式"
msgid "Notification URL List"
msgstr "通知 URL 列表"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知标题"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3239,12 +3243,8 @@ msgid "Notification format"
msgstr "通知格式"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知标题"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "通知 URL 列表"
msgid "Fetch Method"
msgstr "抓取方式"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
@@ -3232,8 +3232,12 @@ msgid "Edit > Watch"
msgstr "編輯 > 監測任務"
#: changedetectionio/forms.py
msgid "Fetch Method"
msgstr "抓取方式"
msgid "Notification URL List"
msgstr "通知 URL 列表"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知標題"
#: changedetectionio/forms.py
msgid "Notification Body"
@@ -3244,12 +3248,8 @@ msgid "Notification format"
msgstr "通知格式"
#: changedetectionio/forms.py
msgid "Notification Title"
msgstr "通知標題"
#: changedetectionio/forms.py
msgid "Notification URL List"
msgstr "通知 URL 列表"
msgid "Fetch Method"
msgstr "抓取方式"
#: changedetectionio/forms.py
msgid "Processor - What do you want to achieve?"
+1 -1
View File
@@ -59,7 +59,7 @@ class TernaryNoneBooleanField(Field):
Custom text can be provided for each option:
- yes_text: Text for True option (default: "Yes")
- no_text: Text for False option (default: "No")
- none_text: Text for None option (default: "Default")
- none_text: Text for None option (default: "Main settings")
"""
widget = TernaryNoneBooleanWidget()
@@ -63,8 +63,8 @@ def test_custom_text():
default_html = default_field.widget(default_field)
print(f"Contains 'Yes': {'Yes' in default_html}")
print(f"Contains 'No': {'No' in default_html}")
print(f"Contains 'Default': {'Default' in default_html}")
assert 'Yes' in default_html and 'No' in default_html and 'Default' in default_html
print(f"Contains 'Main settings': {'Main settings' in default_html}")
assert 'Yes' in default_html and 'No' in default_html and 'Main settings' in default_html
# Test custom text field
print("\n--- Custom Text Field with Emojis ---")