Add price monitor toggle

This commit is contained in:
2026-06-23 16:25:59 +10:00
parent a15d79f12f
commit 647ce87359
4 changed files with 106 additions and 6 deletions
+5
View File
@@ -5,6 +5,11 @@ urlpatterns = [
path('', views.DashboardView.as_view(), name='pricemon-dashboard'),
path('add/', views.WatcherCreateView.as_view(), name='pricemon-add'),
path('<int:pk>/edit/', views.WatcherEditView.as_view(), name='pricemon-edit'),
path(
'<int:pk>/toggle-enabled/',
views.WatcherToggleEnabledView.as_view(),
name='pricemon-toggle-enabled',
),
path('<int:pk>/delete/', views.WatcherDeleteView.as_view(), name='pricemon-delete'),
path('<int:pk>/history/', views.WatcherHistoryView.as_view(), name='pricemon-history'),
path('<int:pk>/check/', views.CheckNowView.as_view(), name='pricemon-check-now'),
+11
View File
@@ -21,6 +21,7 @@ class DashboardView(TemplateView):
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
@@ -75,6 +76,16 @@ class WatcherDeleteView(View):
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'
+35 -6
View File
@@ -35,12 +35,7 @@
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<div class="text-xs text-gray-400 font-medium uppercase tracking-wide mb-1">{% trans "Enabled" %}</div>
<div class="text-2xl font-bold text-gray-900">
{% with watchers|length as total %}
{% for w in watchers %}{% if w.enabled %}{% endif %}{% endfor %}
{{ watchers|length }}
{% endwith %}
</div>
<div class="text-2xl font-bold text-gray-900">{{ enabled_watchers_count }}</div>
</div>
</div>
@@ -147,6 +142,23 @@
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</a>
<form method="post" action="{% url 'pricemon-toggle-enabled' watcher.pk %}" class="inline"
{% if watcher.enabled %}onsubmit="return confirm('Disable price monitoring for this product?')"{% endif %}>
{% csrf_token %}
<button type="submit"
title="{% if watcher.enabled %}{% trans 'Disable monitoring' %}{% else %}{% trans 'Enable monitoring' %}{% endif %}"
class="p-2 rounded text-gray-400 {% if watcher.enabled %}hover:text-amber-600 hover:bg-amber-50{% else %}hover:text-green-600 hover:bg-green-50{% endif %} transition">
{% if watcher.enabled %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m5-3a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
{% else %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5v14l11-7L8 5z"/>
</svg>
{% endif %}
</button>
</form>
<form method="post" action="{% url 'pricemon-delete' watcher.pk %}" class="inline"
onsubmit="return confirm('Delete watcher {{ watcher.name }}?')">
{% csrf_token %}
@@ -262,6 +274,23 @@
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</a>
<form method="post" action="{% url 'pricemon-toggle-enabled' watcher.pk %}" class="inline"
{% if watcher.enabled %}onsubmit="return confirm('Disable price monitoring for this product?')"{% endif %}>
{% csrf_token %}
<button type="submit"
title="{% if watcher.enabled %}{% trans 'Disable monitoring' %}{% else %}{% trans 'Enable monitoring' %}{% endif %}"
class="p-1.5 rounded text-gray-400 {% if watcher.enabled %}hover:text-amber-600 hover:bg-amber-50{% else %}hover:text-green-600 hover:bg-green-50{% endif %} transition">
{% if watcher.enabled %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m5-3a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
{% else %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5v14l11-7L8 5z"/>
</svg>
{% endif %}
</button>
</form>
<form method="post" action="{% url 'pricemon-delete' watcher.pk %}" class="inline"
onsubmit="return confirm('Delete watcher {{ watcher.name }}?')">
{% csrf_token %}
+55
View File
@@ -0,0 +1,55 @@
from unittest.mock import patch
import pytest
from django.urls import reverse
from pricemon.models import PriceWatcher
@pytest.fixture
def watcher():
with patch('pricemon.tasks.schedule_watcher'):
return PriceWatcher.objects.create(
name='Test product',
url='https://example.com/product',
css_selector='.price',
)
@pytest.mark.django_db
class TestPriceMonitorToggle:
def test_dashboard_shows_disable_action_for_enabled_watcher(self, client, watcher):
response = client.get(reverse('pricemon-dashboard'))
assert response.status_code == 200
assert reverse('pricemon-toggle-enabled', args=[watcher.pk]) in response.content.decode()
assert 'Disable monitoring' in response.content.decode()
assert response.context['enabled_watchers_count'] == 1
def test_toggle_disables_watcher_and_unschedules_job(self, client, watcher):
with patch('pricemon.tasks.unschedule_watcher') as unschedule_watcher:
response = client.post(reverse('pricemon-toggle-enabled', args=[watcher.pk]))
watcher.refresh_from_db()
assert response.status_code == 302
assert response.url == reverse('pricemon-dashboard')
assert watcher.enabled is False
unschedule_watcher.assert_called_once()
def test_toggle_enables_paused_watcher_and_schedules_job(self, client, watcher):
with patch('pricemon.tasks.unschedule_watcher'):
watcher.enabled = False
watcher.save(update_fields=['enabled'])
with patch('pricemon.tasks.schedule_watcher') as schedule_watcher:
response = client.post(reverse('pricemon-toggle-enabled', args=[watcher.pk]))
watcher.refresh_from_db()
assert response.status_code == 302
assert watcher.enabled is True
schedule_watcher.assert_called_once()
def test_toggle_rejects_get_requests(self, client, watcher):
response = client.get(reverse('pricemon-toggle-enabled', args=[watcher.pk]))
assert response.status_code == 405