Restock - Threshold change since "first check" was really "since last check", update UI, tests, field name.

This commit is contained in:
dgtlmoon
2026-06-18 11:19:12 +02:00
parent f58d004070
commit da1c39d1bc
33 changed files with 105 additions and 49 deletions
@@ -45,7 +45,7 @@ class Restock(dict):
'in_stock': None,
'price': None,
'currency': None,
'original_price': None
'last_price': None # Price recorded at the most recent check (was misleadingly named 'original_price')
}
# Initialize the dictionary with default values
@@ -59,8 +59,8 @@ class Restock(dict):
raise ValueError("Only one positional argument of type 'dict' is allowed")
def __setitem__(self, key, value):
# Custom logic to handle setting price and original_price
if key == 'price' or key == 'original_price':
# Custom logic to handle setting price and last_price
if key == 'price' or key == 'last_price':
if isinstance(value, str):
value = self.parse_currency(raw_value=value)
@@ -89,7 +89,8 @@ class Watch(BaseWatch):
def extra_notification_token_values(self):
values = super().extra_notification_token_values()
values['restock'] = self.get('restock', {})
# Copy so the derived 'previous_price' token added below doesn't mutate the stored restock object
values['restock'] = dict(self.get('restock', {}))
values['restock']['previous_price'] = None
if self.history_n >= 2:
@@ -109,7 +110,7 @@ class Watch(BaseWatch):
values.append(('restock.price', "Price detected"))
values.append(('restock.in_stock', "In stock status"))
values.append(('restock.original_price', "Original price at first check"))
values.append(('restock.last_price', "Price at the previous check"))
values.append(('restock.previous_price', "Previous price in history"))
return values
@@ -22,7 +22,7 @@ class RestockSettingsForm(Form):
render_kw={"placeholder": _l("No limit"), "size": "10"})
price_change_max = FloatField(_l('Above price to trigger notification'), [validators.Optional()],
render_kw={"placeholder": _l("No limit"), "size": "10"})
price_change_threshold_percent = FloatField(_l('Threshold in %% for price changes since the original price'), validators=[
price_change_threshold_percent = FloatField(_l('Threshold in %% for price changes since the previous check'), validators=[
validators.Optional(),
validators.NumberRange(min=0, max=100, message=_l("Should be between 0 and 100")),
@@ -73,8 +73,8 @@ class processor_settings_form(processor_text_json_diff_form):
</fieldset>
<fieldset class="pure-group price-change-minmax">
{{ render_field(form.processor_config_restock_diff.price_change_threshold_percent) }}
<span class="pure-form-message-inline">Price must change more than this % to trigger a change since the first check.</span><br>
<span class="pure-form-message-inline">For example, If the product is $1,000 USD originally, <strong>2%</strong> would mean it has to change more than $20 since the first check.</span><br>
<span class="pure-form-message-inline">Price must change more than this % since the previous check to trigger a change.</span><br>
<span class="pure-form-message-inline">For example, if the previous check saw the product at $1,000 USD, <strong>2%</strong> would mean it has to change more than $20 since then.</span><br>
</fieldset>
</div>
</fieldset>
@@ -564,10 +564,15 @@ class perform_site_check(difference_detection_processor):
# Main detection method
fetched_md5 = None
# store original price if not set
if itemprop_availability and itemprop_availability.get('price') and not itemprop_availability.get('original_price'):
itemprop_availability['original_price'] = itemprop_availability.get('price')
update_obj['restock']["original_price"] = itemprop_availability.get('price')
# Record this check's price as 'last_price'. The freshly scraped itemprop never carries
# last_price, so this is (re)set on every check - i.e. last_price always holds the price
# from the most recent check, and at comparison time below it is the PREVIOUS check's price.
if itemprop_availability and itemprop_availability.get('price') and not itemprop_availability.get('last_price'):
itemprop_availability['last_price'] = itemprop_availability.get('price')
update_obj['restock']["last_price"] = itemprop_availability.get('price')
logger.debug(
f"{watch.get('uuid')} Updating price - setting 'last_price' to '{itemprop_availability.get('price')}' "
f"(previously stored 'last_price' was '{(watch.get('restock') or {}).get('last_price')}'). ")
if not self.fetcher.instock_data and not itemprop_availability.get('availability') and not itemprop_availability.get('price'):
raise ProcessorException(
@@ -617,9 +622,13 @@ class perform_site_check(difference_detection_processor):
if restock_settings.get('follow_price_changes') and watch.get('restock') and update_obj.get('restock') and update_obj['restock'].get('price'):
price = float(update_obj['restock'].get('price'))
# Default to current price if no previous price found
if watch['restock'].get('original_price'):
previous_price = float(watch['restock'].get('original_price'))
# Compare against last_price (the price from the previous check)
if watch['restock'].get('last_price'):
previous_price = float(watch['restock'].get('last_price'))
logger.debug(
f"{watch.get('uuid')} Comparing NEW price '{price}' against stored 'last_price' '{previous_price}' "
f"(watch's stored current price was '{(watch.get('restock') or {}).get('price')}') -> "
f"price {'CHANGED' if price != previous_price else 'unchanged'}")
# It was different, but negate it further down
if price != previous_price:
changed_detected = True
@@ -642,11 +651,14 @@ class perform_site_check(difference_detection_processor):
else:
logger.trace(f"{watch.get('uuid')} {price} is between {min_limit} and {max_limit}, continuing normal comparison")
# Price comparison by %
if watch['restock'].get('original_price') and changed_detected and restock_settings.get('price_change_threshold_percent'):
previous_price = float(watch['restock'].get('original_price'))
# Price comparison by % - against last_price (the previous check's price)
if watch['restock'].get('last_price') and changed_detected and restock_settings.get('price_change_threshold_percent'):
previous_price = float(watch['restock'].get('last_price'))
pc = float(restock_settings.get('price_change_threshold_percent'))
change = abs((price - previous_price) / previous_price * 100)
logger.debug(
f"{watch.get('uuid')} % threshold check - comparing NEW price '{price}' against stored "
f"'last_price' '{previous_price}' = {change:.3f}% change (threshold {pc}%)")
if change and change <= pc:
logger.debug(f"{watch.get('uuid')} Override change-detected to FALSE because % threshold ({pc}%) was {change:.3f}%")
changed_detected = False
+22
View File
@@ -825,3 +825,25 @@ class DatastoreUpdatesMixin:
self.data['settings']['application']['llm'] = llm
logger.info("update_32: cleaned up obsolete max_tokens_per_check / renamed max_tokens_cumulative")
def update_33(self):
"""Rename restock 'original_price' -> 'last_price'.
The field was named 'original_price' but never held the first-seen price: it was
re-stamped with the current price on every check (the freshly scraped itemprop never
carries it, so the "set if not present" guard was always true). So it always held the
price from the most recent check - i.e. the previous check's price at comparison time.
Renamed so the stored field name matches what it actually contains. Idempotent.
"""
migrated = 0
for uuid, watch in self.data['watching'].items():
restock = watch.get('restock')
if isinstance(restock, dict) and 'original_price' in restock:
# last_price may already exist as the model default (None) after rehydration, so
# only copy the old value across when last_price is still empty; then drop the old key.
if not restock.get('last_price'):
restock['last_price'] = restock.get('original_price')
del restock['original_price']
migrated += 1
if migrated:
logger.info(f"update_33: renamed restock.original_price -> restock.last_price on {migrated} watch(es)")
@@ -298,6 +298,27 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage, d
assert b'1,950.45' in res.data or b'1950.45' in res.data #depending on locale
assert b'has-unread-changes' not in res.data
# PROOF that the threshold is measured "since the PREVIOUS check" and NOT "since the first check":
# a slow upward creep where every single step is below the 5% threshold versus the *previous*
# check, but the total drift from where the creep started (1950.45) ends up ABOVE 5%.
# 1950.45 -> 2000.00 = +2.54% vs previous (below 5%)
# 2000.00 -> 2050.00 = +2.50% vs previous (below 5%)
# 2050.00 -> 2100.00 = +2.44% vs previous (below 5%)
# 1950.45 -> 2100.00 = +7.67% in total (ABOVE 5%)
# Under "since previous check" NONE of these trigger (each step is sub-threshold).
# Under "since first check" the accumulated drift would cross 5% and trigger here - so the
# final assertion below would fail. We deliberately never mark_all_viewed during the creep,
# so any single trigger would leave has-unread-changes set.
for creep_price in ['2000.00', '2050.00', '2100.00']:
set_original_response(props_markup=instock_props[0], price=creep_price, datastore_path=datastore_path)
client.get(url_for("ui.form_watch_checknow"))
wait_for_all_checks(client)
res = client.get(url_for("watchlist.index"))
assert b'2,100.00' in res.data or b'2100.00' in res.data #depending on locale
# +7.67% total drift since the creep started, yet still unread-free -> comparison is vs PREVIOUS check
assert b'has-unread-changes' not in res.data
# Re #2600 - Switch the mode to normal type and back, and see if the values stick..
###################################################################################
@@ -349,9 +370,9 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa
# Should see new tokens register
res = client.get(url_for("settings.settings_page"))
assert b'{{restock.original_price}}' in res.data
assert b'{{restock.last_price}}' in res.data
assert b'{{restock.previous_price}}' in res.data
assert b'Original price at first check' in res.data
assert b'Price at the previous check' in res.data
#####################
# Set this up for when we remove the notification from the watch, it should fallback with these details
@@ -2436,7 +2436,7 @@ msgstr "Ціна вище для спрацювання сповіщення"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgid "Threshold in % for price changes since the original price"
msgstr "Поріг у %% для зміни ціни від початкової"
#: changedetectionio/processors/restock_diff/forms.py
@@ -3340,8 +3340,8 @@ msgstr "Vyšší cena pro spuštění upozornění"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Prahová hodnota v %% pro změny ceny od původní ceny"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3392,8 +3392,8 @@ msgstr "Über dem Preis, um eine Benachrichtigung auszulösen"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Schwellenwert in %% für Preisänderungen seit dem ursprünglichen Preis"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3332,7 +3332,7 @@ msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
@@ -3332,7 +3332,7 @@ msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
@@ -3405,8 +3405,8 @@ msgstr "Precio superior para activar la notificación"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Umbral en %% fo cambios de precio desde el precio original"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3345,8 +3345,8 @@ msgstr "Au-dessus du prix pour déclencher une notification"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Seuil en %% pour les changements de prix depuis le prix initial"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3334,8 +3334,8 @@ msgstr "Prezzo massimo per attivare notifica"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Soglia in %% per modifiche prezzo dal prezzo originale"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3351,8 +3351,8 @@ msgstr "通知をトリガーする上限価格"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "元の価格からの価格変動率のしきい値(%%)"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3342,8 +3342,8 @@ msgstr "다음 가격 초과이면 알림 트리거"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "원래 가격 대비 가격 변동 기준(%%)"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
+2 -2
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.55.7\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-06-03 12:07+0200\n"
"POT-Creation-Date: 2026-06-18 11:19+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"
@@ -3331,7 +3331,7 @@ msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
@@ -3382,8 +3382,8 @@ msgstr "Preço acima deste valor para disparar notificação"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Limite em %% para mudanças de preço desde o preço original"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3385,8 +3385,8 @@ msgstr "Bildirimi tetiklemek için fiyatın üstünde"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Orijinal fiyattan bu yana fiyat değişiklikleri için %% cinsinden eşik"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3364,8 +3364,8 @@ msgstr "Ціна вище для спрацювання сповіщення"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "Поріг у %% для зміни ціни від початкової"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3337,8 +3337,8 @@ msgstr "高于该价格时触发通知"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "相对原始价格的变动阈值(百分比)"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"
@@ -3336,8 +3336,8 @@ msgstr "高於此價格觸發通知"
#: changedetectionio/processors/restock_diff/forms.py
#, python-format
msgid "Threshold in %% for price changes since the original price"
msgstr "自原始價格以來價格變化的閾值(%%"
msgid "Threshold in %% for price changes since the previous check"
msgstr ""
#: changedetectionio/processors/restock_diff/forms.py
msgid "Should be between 0 and 100"