diff --git a/static/js/offline-tasks.js b/static/js/offline-tasks.js index 916741b..bdff02d 100644 --- a/static/js/offline-tasks.js +++ b/static/js/offline-tasks.js @@ -117,7 +117,14 @@ function escapeHtml(str) { function isTaskOverdue(task) { // Mirrors Task.is_overdue in tasks/models.py. if (!task.due_date || task.status === 'completed' || task.status === 'cancelled') return false; - return task.due_date < todayLocalStr(); + const today = todayLocalStr(); + if (task.due_date < today) return true; + if (task.due_date === today && task.due_time) { + const now = new Date(); + const nowStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; + return task.due_time < nowStr; + } + return false; } function renderTaskItem(task, tagsBySyncId) { diff --git a/tasks/models.py b/tasks/models.py index ccc6f69..fb3d051 100644 --- a/tasks/models.py +++ b/tasks/models.py @@ -155,16 +155,19 @@ class Task(models.Model): from django.utils import timezone from zoneinfo import ZoneInfo - # Get current date in user's timezone + # Get current date/time in user's timezone try: user_tz = ZoneInfo(self.user.timezone) user_now = timezone.now().astimezone(user_tz) - user_today = user_now.date() except (Exception,): # Fall back to UTC if user timezone is invalid or missing - user_today = timezone.now().date() + user_now = timezone.now() - return self.due_date < user_today + if self.due_date < user_now.date(): + return True + if self.due_date == user_now.date() and self.due_time: + return self.due_time < user_now.time() + return False return False @property diff --git a/tasks/tests.py b/tasks/tests.py index 6bd8a97..9359a45 100644 --- a/tasks/tests.py +++ b/tasks/tests.py @@ -1,7 +1,9 @@ -from datetime import date +from datetime import date, datetime, time as dt_time +from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase +from django.utils import timezone as django_timezone from tasks.models import Task @@ -85,3 +87,49 @@ class CustomRecurrenceTests(TestCase): parsed = task.parsed_custom_recurrence self.assertIsNone(parsed['freq']) self.assertEqual(parsed['byweekday'], []) + + +class IsOverdueTests(TestCase): + """Tests for Task.is_overdue, which must account for due_time, not just due_date (Gitea #7).""" + + def setUp(self): + self.user = User.objects.create_user( + username='overdueuser', + email='overdueuser@example.com', + password='testpass123', + ) + + def make_task(self, **kwargs): + defaults = {'user': self.user, 'title': 'Test task', 'status': 'pending'} + defaults.update(kwargs) + return Task.objects.create(**defaults) + + @patch('django.utils.timezone.now') + def test_not_overdue_before_due_time_same_day(self, mock_now): + mock_now.return_value = django_timezone.make_aware(datetime(2026, 1, 15, 8, 0, 0)) + task = self.make_task(due_date=date(2026, 1, 15), due_time=dt_time(17, 0, 0)) + self.assertFalse(task.is_overdue) + + @patch('django.utils.timezone.now') + def test_overdue_after_due_time_same_day(self, mock_now): + mock_now.return_value = django_timezone.make_aware(datetime(2026, 1, 15, 18, 0, 0)) + task = self.make_task(due_date=date(2026, 1, 15), due_time=dt_time(17, 0, 0)) + self.assertTrue(task.is_overdue) + + @patch('django.utils.timezone.now') + def test_not_overdue_same_day_without_due_time(self, mock_now): + mock_now.return_value = django_timezone.make_aware(datetime(2026, 1, 15, 23, 59, 0)) + task = self.make_task(due_date=date(2026, 1, 15), due_time=None) + self.assertFalse(task.is_overdue) + + @patch('django.utils.timezone.now') + def test_overdue_once_date_has_passed(self, mock_now): + mock_now.return_value = django_timezone.make_aware(datetime(2026, 1, 16, 0, 1, 0)) + task = self.make_task(due_date=date(2026, 1, 15), due_time=dt_time(23, 0, 0)) + self.assertTrue(task.is_overdue) + + @patch('django.utils.timezone.now') + def test_completed_task_never_overdue(self, mock_now): + mock_now.return_value = django_timezone.make_aware(datetime(2026, 1, 15, 23, 0, 0)) + task = self.make_task(due_date=date(2026, 1, 1), due_time=dt_time(9, 0, 0), status='completed') + self.assertFalse(task.is_overdue) diff --git a/templates/sw.js b/templates/sw.js index 4d173b1..0650054 100644 --- a/templates/sw.js +++ b/templates/sw.js @@ -1,4 +1,4 @@ -{% load static %}const CACHE_NAME = 'keepitgoing-shell-v8'; +{% load static %}const CACHE_NAME = 'keepitgoing-shell-v10'; const OFFLINE_URL = '{% url "offline" %}'; const OFFLINE_TASKS_URL = '{% url "offline-tasks" %}'; @@ -18,7 +18,19 @@ const PRECACHE_URLS = [ self.addEventListener('install', (event) => { event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) + caches.open(CACHE_NAME).then((cache) => + // cache.addAll() lets the browser's own HTTP cache satisfy each + // fetch - static assets here have no Cache-Control/ETag (only + // Last-Modified), so a heuristically-cached stale response can + // silently win even on a CACHE_NAME bump. { cache: 'reload' } + // forces a real network round-trip so precache always reflects + // what the server currently serves. + Promise.all( + PRECACHE_URLS.map((url) => + fetch(url, { cache: 'reload' }).then((response) => cache.put(url, response)) + ) + ) + ) ); self.skipWaiting(); });