mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
from decimal import Decimal, ROUND_HALF_UP
|
|
|
|
from django.db import models
|
|
|
|
|
|
class PriceWatcher(models.Model):
|
|
INTERVAL_CHOICES = [
|
|
(6, 'Every 6 hours'),
|
|
(12, 'Every 12 hours'),
|
|
(24, 'Every day'),
|
|
(48, 'Every 2 days'),
|
|
(72, 'Every 3 days'),
|
|
(168, 'Every 7 days'),
|
|
]
|
|
|
|
name = models.CharField(max_length=200)
|
|
url = models.URLField(max_length=2000)
|
|
css_selector = models.CharField(
|
|
max_length=500,
|
|
help_text='CSS selector for the price element, e.g. span.price or .product-price',
|
|
)
|
|
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,
|
|
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).'
|
|
),
|
|
)
|
|
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)
|
|
|
|
class Meta:
|
|
ordering = ['name']
|
|
|
|
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')
|
|
price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
raw_text = models.CharField(max_length=500, blank=True)
|
|
error = models.CharField(max_length=500, blank=True)
|
|
checked_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-checked_at']
|
|
|
|
def __str__(self):
|
|
return f'{self.watcher.name} @ {self.checked_at:%Y-%m-%d %H:%M}'
|
|
|
|
|
|
class PriceAlert(models.Model):
|
|
watcher = models.ForeignKey(PriceWatcher, on_delete=models.CASCADE, related_name='alerts')
|
|
old_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
new_price = models.DecimalField(max_digits=12, decimal_places=2)
|
|
drop_pct = models.DecimalField(max_digits=5, decimal_places=2, default=0)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
dismissed = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f'{self.watcher.name}: ${self.old_price} → ${self.new_price}'
|