From 575bdcfbe83c68b80debc1112d37591136ead46f Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Wed, 12 Jun 2024 18:11:20 +0200 Subject: [PATCH] WIP --- changedetectionio/forms.py | 21 ++++++++---- changedetectionio/model/__init__.py | 1 + changedetectionio/processors/restock_diff.py | 33 +++++++++++++++---- .../tests/test_restock_itemprop.py | 9 +++-- .../tests/unit/test_restock_logic.py | 21 ++++++++++++ 5 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 changedetectionio/tests/unit/test_restock_logic.py diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 862e75d2..defac319 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -519,13 +519,17 @@ class processor_text_json_diff_form(commonSettingsForm): result = False return result + class processor_restock_diff_form(processor_text_json_diff_form): + in_stock_only = BooleanField('Only trigger when product goes BACK to in-stock', default=True) + price_change_min = FloatField('Minimum amount to trigger notification', [validators.Optional()], render_kw={"placeholder": "No limit", "size": "10"}) + price_change_max = FloatField('Maximum amount to trigger notification', [validators.Optional()], render_kw={"placeholder": "No limit", "size": "10"}) + price_change_threshold_percent = FloatField('Threshold in % for price changes', validators=[ + validators.Optional(), + validators.NumberRange(min=0, max=100, message="Should be between 0 and 100"), + ], render_kw={"placeholder": "0%", "size": "5"}) - #@todo - add "Any increase" and "Any decrease" options - in_stock_only = BooleanField('Only trigger when product goes BACK to in-stock', default=True) - price_change_min = FloatField('Minimum amount to trigger notification',[validators.Optional()], render_kw={"placeholder": "No limit"} ) - price_change_max = FloatField('Maximum amount to trigger notification',[validators.Optional()], render_kw={"placeholder": "No limit"} ) follow_price_changes = BooleanField('Follow price changes', default=False) def extra_tab_content(self): @@ -551,14 +555,19 @@ class processor_restock_diff_form(processor_text_json_diff_form): {{ render_checkbox_field(form.follow_price_changes) }} Changes in price should trigger a notification When OFF - only care about restock detection - +
{{ render_field(form.price_change_min) }} Minimum amount, only trigger a change when the price is less than this amount. -
+
{{ render_field(form.price_change_max) }} Maximum amount, only trigger a change when the price is more than this amount. +
+
+ {{ render_field(form.price_change_threshold_percent) }} + Price must change more than this % to trigger a change.
+ For example, If the product is $1,000 USD, 2% would mean it has to change more than $20 since the last check.
""" diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index 94f8e00c..0b43bcc5 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -58,6 +58,7 @@ class watch_base(dict): 'previous_md5': False, 'previous_md5_before_filters': False, # Used for skipping changedetection entirely 'processor': 'text_json_diff', # could be restock_diff or others from .processors + 'price_change_threshold_percent': None, 'proxy': None, # Preferred proxy connection 'remote_server_reply': None, # From 'server' reply header 'sort_text_alphabetically': False, diff --git a/changedetectionio/processors/restock_diff.py b/changedetectionio/processors/restock_diff.py index 918085f1..b9f3e60b 100644 --- a/changedetectionio/processors/restock_diff.py +++ b/changedetectionio/processors/restock_diff.py @@ -87,6 +87,22 @@ def get_itemprop_availability(html_content) -> Restock: return Restock(value) + +def is_between(number, lower=None, upper=None): + """ + Check if a number is between two values. + + Parameters: + number (float): The number to check. + lower (float or None): The lower bound (inclusive). If None, no lower bound. + upper (float or None): The upper bound (inclusive). If None, no upper bound. + + Returns: + bool: True if the number is between the lower and upper bounds, False otherwise. + """ + return (lower is None or lower <= number) and (upper is None or number <= upper) + + class perform_site_check(difference_detection_processor): screenshot = None xpath_data = None @@ -161,7 +177,7 @@ class perform_site_check(difference_detection_processor): # All cases changed_detected = True - if watch.get('follow_price_changes') and watch.get('restock') and update_obj['restock'].get('price'): + if watch.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')) previous_price = float(watch['restock'].get('price')) @@ -170,18 +186,21 @@ class perform_site_check(difference_detection_processor): changed_detected = True # Minimum/maximum price limit - if update_obj.get('restock') and update_obj['restock'].get('price') and watch.get('price_change_min'): + if update_obj.get('restock') and update_obj['restock'].get('price'): logger.debug( f"{uuid} - Change was detected, 'price_change_max' is '{watch.get('price_change_max', '')}' 'price_change_min' is '{watch.get('price_change_min', '')}', price from website is '{update_obj['restock'].get('price', '')}'.") if update_obj['restock'].get('price'): - min_limit = float(watch.get('price_change_min', 0)) - max_limit = float(watch.get('price_change_max', float('inf'))) # Set to infinity if not provided + min_limit = float(watch.get('price_change_min')) if watch.get('price_change_min') else None + max_limit = float(watch.get('price_change_max')) if watch.get('price_change_max') else None price = float(update_obj['restock'].get('price')) logger.debug(f"{uuid} after float conversion - Min limit: '{min_limit}' Max limit: '{max_limit}' Price: '{price}'") - if changed_detected: - if price < max_limit and price > min_limit: - changed_detected = False + if min_limit or max_limit: + if is_between(number=price, lower=min_limit, upper=max_limit): + if changed_detected: + logger.debug( + f"{uuid} Override change-detected to FALSE because price was inside threshold") + changed_detected = False # Always record the new checksum update_obj["previous_md5"] = fetched_md5 diff --git a/changedetectionio/tests/test_restock_itemprop.py b/changedetectionio/tests/test_restock_itemprop.py index 596e31b3..1a673df1 100644 --- a/changedetectionio/tests/test_restock_itemprop.py +++ b/changedetectionio/tests/test_restock_itemprop.py @@ -80,7 +80,7 @@ def test_restock_itemprop_basic(client, live_server): assert b'Deleted' in res.data def test_itemprop_price_change(client, live_server): -# live_server_setup(live_server) + #live_server_setup(live_server) test_url = url_for('test_endpoint', _external=True) @@ -123,10 +123,12 @@ def test_itemprop_price_change(client, live_server): res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) assert b'Deleted' in res.data - def test_itemprop_price_minmax_limit(client, live_server): #live_server_setup(live_server) + res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) + assert b'Deleted' in res.data + test_url = url_for('test_endpoint', _external=True) set_original_response(props_markup=instock_props[0], price="950.95") @@ -184,3 +186,6 @@ def test_itemprop_price_minmax_limit(client, live_server): res = client.get(url_for("index")) assert b'1890.45' in res.data assert b'unviewed' in res.data + res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) + assert b'Deleted' in res.data + diff --git a/changedetectionio/tests/unit/test_restock_logic.py b/changedetectionio/tests/unit/test_restock_logic.py new file mode 100644 index 00000000..c189844f --- /dev/null +++ b/changedetectionio/tests/unit/test_restock_logic.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 + +# run from dir above changedetectionio/ dir +# python3 -m unittest changedetectionio.tests.unit.test_restock_logic + +import unittest +import os + +from changedetectionio.processors import restock_diff + +# mostly +class TestDiffBuilder(unittest.TestCase): + + def test_logic(self): + assert restock_diff.is_between(number=10, lower=9, upper=11) == True, "Between 9 and 11" + assert restock_diff.is_between(number=10, lower=0, upper=11) == True, "Between 9 and 11" + assert restock_diff.is_between(number=10, lower=None, upper=11) == True, "Between None and 11" + assert not restock_diff.is_between(number=12, lower=None, upper=11) == True, "12 is not between None and 11" + +if __name__ == '__main__': + unittest.main()