mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-07-08 08:21:14 +00:00
Restock - Threshold change since "first check" was really "since last check", update UI, tests, field name.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user