Files
links/pricemon/views.py
T
2026-06-23 16:25:59 +10:00

173 lines
6.3 KiB
Python

import logging
from django.contrib import messages
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views import View
from django.views.generic import TemplateView
from pricemon.forms import PriceWatcherForm
from pricemon.models import PriceAlert, PriceSnapshot, PriceWatcher
logger = logging.getLogger(__name__)
class DashboardView(TemplateView):
template_name = 'pricemon/dashboard.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
watchers = PriceWatcher.objects.all()
active_alerts = PriceAlert.objects.filter(dismissed=False).select_related('watcher')
ctx['watchers'] = watchers
ctx['total_watchers'] = watchers.count()
ctx['enabled_watchers_count'] = watchers.filter(enabled=True).count()
ctx['active_alerts_count'] = active_alerts.count()
ctx['active_alerts'] = active_alerts[:10]
return ctx
class WatcherCreateView(View):
template_name = 'pricemon/watcher_form.html'
def get(self, request):
return render(request, self.template_name, {'form': PriceWatcherForm(), 'title': 'Add Watcher'})
def post(self, request):
form = PriceWatcherForm(request.POST)
if form.is_valid():
watcher = form.save()
messages.success(request, f'Watcher "{watcher.name}" created.')
return redirect('pricemon-dashboard')
return render(request, self.template_name, {'form': form, 'title': 'Add Watcher'})
class WatcherEditView(View):
template_name = 'pricemon/watcher_form.html'
def get(self, request, pk):
watcher = get_object_or_404(PriceWatcher, pk=pk)
return render(request, self.template_name, {
'form': PriceWatcherForm(instance=watcher),
'watcher': watcher,
'title': f'Edit — {watcher.name}',
})
def post(self, request, pk):
watcher = get_object_or_404(PriceWatcher, pk=pk)
form = PriceWatcherForm(request.POST, instance=watcher)
if form.is_valid():
form.save()
messages.success(request, f'Watcher "{watcher.name}" updated.')
return redirect('pricemon-dashboard')
return render(request, self.template_name, {
'form': form,
'watcher': watcher,
'title': f'Edit — {watcher.name}',
})
class WatcherDeleteView(View):
def post(self, request, pk):
watcher = get_object_or_404(PriceWatcher, pk=pk)
name = watcher.name
watcher.delete()
messages.success(request, f'Watcher "{name}" deleted.')
return redirect('pricemon-dashboard')
class WatcherToggleEnabledView(View):
def post(self, request, pk):
watcher = get_object_or_404(PriceWatcher, pk=pk)
watcher.enabled = not watcher.enabled
watcher.save(update_fields=['enabled'])
state = 'enabled' if watcher.enabled else 'disabled'
messages.success(request, f'Watcher "{watcher.name}" {state}.')
return redirect('pricemon-dashboard')
class WatcherHistoryView(TemplateView):
template_name = 'pricemon/history.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
watcher = get_object_or_404(PriceWatcher, pk=kwargs['pk'])
ctx['watcher'] = watcher
ctx['snapshots'] = watcher.snapshots.all()[:200]
return ctx
class CheckNowView(View):
def post(self, request, pk):
from pricemon.tasks import check_watcher
watcher = get_object_or_404(PriceWatcher, pk=pk)
try:
check_watcher(watcher.pk)
watcher.refresh_from_db()
msg = f'Check complete. Current price: ${watcher.last_price}' if watcher.last_price else 'Check complete (price not detected).'
if request.headers.get('HX-Request'):
return JsonResponse({'ok': True, 'message': msg})
messages.success(request, msg)
except Exception as exc:
logger.error(f'Manual check failed for watcher {pk}: {exc}')
if request.headers.get('HX-Request'):
return JsonResponse({'ok': False, 'message': str(exc)}, status=500)
messages.error(request, f'Check failed: {exc}')
return redirect('pricemon-dashboard')
class AlertsPartialView(View):
def get(self, request):
alerts = PriceAlert.objects.filter(dismissed=False).select_related('watcher')[:20]
return render(request, 'pricemon/_alerts.html', {'alerts': alerts})
class DismissAlertView(View):
def post(self, request, pk):
alert = get_object_or_404(PriceAlert, pk=pk)
alert.dismissed = True
alert.save(update_fields=['dismissed'])
if request.headers.get('HX-Request'):
return HttpResponse('')
return redirect('pricemon-dashboard')
class ChartDataView(View):
def get(self, request, pk):
watcher = get_object_or_404(PriceWatcher, pk=pk)
snapshots = (
watcher.snapshots
.filter(price__isnull=False)
.values('checked_at', 'price')
.order_by('checked_at')[:500]
)
labels = [s['checked_at'].strftime('%Y-%m-%d %H:%M') for s in snapshots]
prices = [float(s['price']) for s in snapshots]
return JsonResponse({'labels': labels, 'prices': prices, 'name': watcher.name})
class TestSelectorView(View):
def post(self, request):
from pricemon.scraper import _fetch_with_requests, _fetch_with_playwright, parse_price
url = request.POST.get('url', '').strip()
css_selector = request.POST.get('css_selector', '').strip()
if not url or not css_selector:
return render(request, 'pricemon/_selector_test_result.html', {
'error': 'Both URL and CSS selector are required.',
})
raw_text, error = _fetch_with_requests(url, css_selector)
method = 'requests'
if not raw_text and not error:
raw_text, error = _fetch_with_playwright(url, css_selector)
method = 'playwright'
price = parse_price(raw_text) if raw_text else None
return render(request, 'pricemon/_selector_test_result.html', {
'raw_text': raw_text,
'price': price,
'error': error,
'method': method,
})