diff --git a/pricemon/admin.py b/pricemon/admin.py index 61c43d7..9246ee1 100644 --- a/pricemon/admin.py +++ b/pricemon/admin.py @@ -4,7 +4,15 @@ from pricemon.models import PriceAlert, PriceSnapshot, PriceWatcher @admin.register(PriceWatcher) class PriceWatcherAdmin(admin.ModelAdmin): - list_display = ['name', 'url', 'last_price', 'last_checked_at', 'enabled', 'check_interval_hours'] + list_display = [ + 'name', + 'url', + 'initial_price', + 'last_price', + 'last_checked_at', + 'enabled', + 'check_interval_hours', + ] list_filter = ['enabled', 'check_interval_hours'] search_fields = ['name', 'url'] diff --git a/pricemon/migrations/0005_pricewatcher_initial_price.py b/pricemon/migrations/0005_pricewatcher_initial_price.py new file mode 100644 index 0000000..b6ba352 --- /dev/null +++ b/pricemon/migrations/0005_pricewatcher_initial_price.py @@ -0,0 +1,41 @@ +from django.db import migrations, models + + +def backfill_initial_price(apps, schema_editor): + PriceWatcher = apps.get_model('pricemon', 'PriceWatcher') + PriceSnapshot = apps.get_model('pricemon', 'PriceSnapshot') + + for watcher in PriceWatcher.objects.all(): + if watcher.initial_price is not None: + continue + + snapshot = ( + PriceSnapshot.objects.filter(watcher_id=watcher.pk, price__isnull=False) + .order_by('checked_at', 'pk') + .first() + ) + if snapshot is None: + continue + + watcher.initial_price = snapshot.price + watcher.save(update_fields=['initial_price']) + + +def noop_reverse(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('pricemon', '0004_alert_price_threshold_recurring'), + ] + + operations = [ + migrations.AddField( + model_name='pricewatcher', + name='initial_price', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True), + ), + migrations.RunPython(backfill_initial_price, noop_reverse), + ] diff --git a/pricemon/models.py b/pricemon/models.py index fa73257..39c828b 100644 --- a/pricemon/models.py +++ b/pricemon/models.py @@ -1,3 +1,5 @@ +from decimal import Decimal, ROUND_HALF_UP + from django.db import models @@ -19,15 +21,22 @@ class PriceWatcher(models.Model): ) check_interval_hours = models.IntegerField(choices=INTERVAL_CHOICES, default=24) alert_price_threshold = models.DecimalField( - max_digits=12, decimal_places=2, null=True, blank=True, + max_digits=12, + decimal_places=2, + null=True, + blank=True, help_text='Send an alert when the detected price is at or below this value. Leave blank to disable alerts.', ) recurring_notification = models.BooleanField( default=True, - help_text='Keep notifying on every check while the price meets the threshold. Disable to notify only once (until you dismiss the alert).', + help_text=( + 'Keep notifying on every check while the price meets the threshold. ' + 'Disable to notify only once (until you dismiss the alert).' + ), ) enabled = models.BooleanField(default=True) last_checked_at = models.DateTimeField(null=True, blank=True) + initial_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) last_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) @@ -37,6 +46,34 @@ class PriceWatcher(models.Model): def __str__(self): return self.name + @property + def price_change_direction(self): + if self.initial_price is None or self.last_price is None: + return None + if self.last_price > self.initial_price: + return 'up' + if self.last_price < self.initial_price: + return 'down' + return 'flat' + + @property + def price_change_pct(self): + if self.initial_price is None or self.last_price is None or self.initial_price == 0: + return None + pct = ((self.last_price - self.initial_price) / self.initial_price) * Decimal('100') + return abs(pct).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + + @property + def price_change_symbol(self): + direction = self.price_change_direction + if direction == 'up': + return '↑' + if direction == 'down': + return '↓' + if direction == 'flat': + return '→' + return '' + class PriceSnapshot(models.Model): watcher = models.ForeignKey(PriceWatcher, on_delete=models.CASCADE, related_name='snapshots') diff --git a/pricemon/tasks.py b/pricemon/tasks.py index 138a0b0..74691dc 100644 --- a/pricemon/tasks.py +++ b/pricemon/tasks.py @@ -53,6 +53,9 @@ def check_watcher(watcher_pk: int) -> None: update_fields = ['last_checked_at'] if price is not None: + if watcher.initial_price is None: + watcher.initial_price = price + update_fields.append('initial_price') watcher.last_price = price update_fields.append('last_price') watcher.last_checked_at = now() diff --git a/templates/pricemon/dashboard.html b/templates/pricemon/dashboard.html index a087d4b..5bc6181 100644 --- a/templates/pricemon/dashboard.html +++ b/templates/pricemon/dashboard.html @@ -92,6 +92,29 @@ {% else %} — {% endif %} +