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, })