From 7b7f17fde6cc6c45a36752cbf1b2752fe396476d Mon Sep 17 00:00:00 2001 From: "Christopher C. Wells" Date: Sun, 14 Aug 2022 13:55:57 -0700 Subject: [PATCH] Remove "inactive" timer concept -- delete on complete Closes #109 --- api/filters.py | 2 +- api/serializers.py | 11 +- api/tests.py | 41 +--- api/views.py | 6 - babybuddy/fixtures/tests.json | 3 - core/admin.py | 4 +- core/forms.py | 4 +- ..._options_remove_timer_duration_and_more.py | 38 ++++ core/models.py | 36 +-- core/static_src/js/timer.js | 8 +- .../core/timer_confirm_delete_inactive.html | 26 --- core/templates/core/timer_detail.html | 21 +- core/templates/core/timer_list.html | 7 - core/templates/core/timer_nav.html | 2 +- core/templatetags/timers.py | 7 +- core/tests/tests_forms.py | 16 +- core/tests/tests_models.py | 26 +-- core/tests/tests_views.py | 17 -- core/urls.py | 6 - core/views.py | 45 +--- dashboard/templates/cards/timer_list.html | 6 +- dashboard/templatetags/cards.py | 4 +- dashboard/tests/tests_templatetags.py | 2 +- docs/api.md | 22 +- docs/user-guide/getting-started.md | 2 +- static/babybuddy/js/app.d20500d757a5.js | 206 +++++++++++++++++ static/babybuddy/js/app.d20500d757a5.js.gz | Bin 0 -> 1554 bytes static/babybuddy/js/app.js | 207 +++++++++++++++++- static/babybuddy/js/app.js.gz | Bin 923 -> 1554 bytes static/staticfiles.json | 2 +- 30 files changed, 502 insertions(+), 275 deletions(-) create mode 100644 core/migrations/0027_alter_timer_options_remove_timer_duration_and_more.py delete mode 100644 core/templates/core/timer_confirm_delete_inactive.html create mode 100644 static/babybuddy/js/app.d20500d757a5.js create mode 100644 static/babybuddy/js/app.d20500d757a5.js.gz diff --git a/api/filters.py b/api/filters.py index b95c15e8..f1b1bcf1 100644 --- a/api/filters.py +++ b/api/filters.py @@ -99,7 +99,7 @@ class TemperatureFilter(TimeFieldFilter, TagsFieldFilter): class TimerFilter(StartEndFieldFilter): class Meta(StartEndFieldFilter.Meta): model = models.Timer - fields = sorted(StartEndFieldFilter.Meta.fields + ["active", "user"]) + fields = sorted(StartEndFieldFilter.Meta.fields + ["user"]) class TummyTimeFilter(StartEndFieldFilter, TagsFieldFilter): diff --git a/api/serializers.py b/api/serializers.py index 23c46fe6..af3982b6 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -77,16 +77,12 @@ class CoreModelWithDurationSerializer(CoreModelSerializer): timer = attrs["timer"] attrs.pop("timer") - if timer.end: - end = timer.end - else: - end = timezone.now() if timer.child: attrs["child"] = timer.child # Overwrites values provided directly! attrs["start"] = timer.start - attrs["end"] = end + attrs["end"] = timezone.now() # The "child", "start", and "end" field should all be set at this # point. If one is not, model validation will fail because they are @@ -103,7 +99,7 @@ class CoreModelWithDurationSerializer(CoreModelSerializer): # Only actually stop the timer if all validation passed. if timer: - timer.stop(attrs["end"]) + timer.stop() return attrs @@ -232,10 +228,11 @@ class TimerSerializer(CoreModelSerializer): queryset=get_user_model().objects.all(), required=False, ) + duration = serializers.DurationField(read_only=True, required=False) class Meta: model = models.Timer - fields = ("id", "child", "name", "start", "end", "duration", "active", "user") + fields = ("id", "child", "name", "start", "duration", "user") def validate(self, attrs): attrs = super(TimerSerializer, self).validate(attrs) diff --git a/api/tests.py b/api/tests.py index ed5f8505..535d1efa 100644 --- a/api/tests.py +++ b/api/tests.py @@ -47,7 +47,6 @@ class TestBase: ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) timer.refresh_from_db() - self.assertTrue(timer.active) child = models.Child.objects.first() self.timer_test_data["child"] = child.id @@ -55,11 +54,9 @@ class TestBase: self.endpoint, self.timer_test_data, format="json" ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - timer.refresh_from_db() - self.assertFalse(timer.active) obj = self.model.objects.get(pk=response.data["id"]) self.assertEqual(obj.start, start) - self.assertEqual(obj.end, timer.end) + self.assertIsNotNone(obj.end) def test_post_with_timer_with_child(self): if not self.timer_test_data: @@ -73,12 +70,10 @@ class TestBase: self.endpoint, self.timer_test_data, format="json" ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - timer.refresh_from_db() - self.assertFalse(timer.active) obj = self.model.objects.get(pk=response.data["id"]) - self.assertEqual(obj.child, timer.child) + self.assertIsNotNone(obj.child) self.assertEqual(obj.start, start) - self.assertEqual(obj.end, timer.end) + self.assertIsNotNone(obj.end) class BMIAPITestCase(TestBase.BabyBuddyAPITestCaseBase): @@ -703,19 +698,7 @@ class TimerAPITestCase(TestBase.BabyBuddyAPITestCaseBase): def test_get(self): response = self.client.get(self.endpoint) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.data["results"][0], - { - "id": 1, - "child": None, - "name": "Fake timer", - "start": "2017-11-17T23:30:00-05:00", - "end": "2017-11-18T00:30:00-05:00", - "duration": "01:00:00", - "active": False, - "user": 1, - }, - ) + self.assertEqual(response.data["results"][0]["id"], 1) def test_post(self): data = {"name": "New fake timer", "user": 1} @@ -743,31 +726,19 @@ class TimerAPITestCase(TestBase.BabyBuddyAPITestCaseBase): }, ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, entry) + self.assertEqual(response.data["name"], entry["name"]) - def test_start_stop_timer(self): + def test_start_restart_timer(self): endpoint = "{}{}/".format(self.endpoint, 1) response = self.client.get(endpoint) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(response.data["active"]) response = self.client.patch(f"{endpoint}restart/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data["active"]) # Restart twice is allowed response = self.client.patch(f"{endpoint}restart/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data["active"]) - - response = self.client.patch(f"{endpoint}stop/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(response.data["active"]) - - # Stopping twice is allowed, too - response = self.client.patch(f"{endpoint}stop/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(response.data["active"]) class TummyTimeAPITestCase(TestBase.BabyBuddyAPITestCaseBase): diff --git a/api/views.py b/api/views.py index edad1ed6..7f513e92 100644 --- a/api/views.py +++ b/api/views.py @@ -118,12 +118,6 @@ class TimerViewSet(viewsets.ModelViewSet): ordering_fields = ("duration", "end", "start") ordering = "-start" - @action(detail=True, methods=["patch"]) - def stop(self, request, pk=None): - timer = self.get_object() - timer.stop() - return Response(self.serializer_class(timer).data) - @action(detail=True, methods=["patch"]) def restart(self, request, pk=None): timer = self.get_object() diff --git a/babybuddy/fixtures/tests.json b/babybuddy/fixtures/tests.json index 1eed6953..711e9aa0 100644 --- a/babybuddy/fixtures/tests.json +++ b/babybuddy/fixtures/tests.json @@ -393,9 +393,6 @@ { "name": "Fake timer", "start": "2017-11-18T04:30:00Z", - "end": "2017-11-18T05:30:00Z", - "duration": "01:00:00", - "active": false, "user": 1 } }, diff --git a/core/admin.py b/core/admin.py index 7f2d8757..69550068 100644 --- a/core/admin.py +++ b/core/admin.py @@ -216,8 +216,8 @@ class TemperatureAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin): @admin.register(models.Timer) class TimerAdmin(admin.ModelAdmin): - list_display = ("name", "child", "start", "end", "duration", "active", "user") - list_filter = ("child", "active", "user") + list_display = ("name", "child", "start", "duration", "user") + list_filter = ("child", "user") search_fields = ("child__first_name", "child__last_name", "name", "user") diff --git a/core/forms.py b/core/forms.py index 22c8e65d..06b49602 100644 --- a/core/forms.py +++ b/core/forms.py @@ -44,7 +44,7 @@ def set_initial_values(kwargs, form_type): if timer_id: timer = models.Timer.objects.get(id=timer_id) kwargs["initial"].update( - {"timer": timer, "start": timer.start, "end": timer.end or timezone.now()} + {"timer": timer, "start": timer.start, "end": timezone.now()} ) # Set type and method values for Feeding instance based on last feed. @@ -83,7 +83,7 @@ class CoreModelForm(forms.ModelForm): instance = super(CoreModelForm, self).save(commit=False) if self.timer_id: timer = models.Timer.objects.get(id=self.timer_id) - timer.stop(instance.end) + timer.stop() if commit: instance.save() self.save_m2m() diff --git a/core/migrations/0027_alter_timer_options_remove_timer_duration_and_more.py b/core/migrations/0027_alter_timer_options_remove_timer_duration_and_more.py new file mode 100644 index 00000000..bed2cb2e --- /dev/null +++ b/core/migrations/0027_alter_timer_options_remove_timer_duration_and_more.py @@ -0,0 +1,38 @@ +from django.db import migrations + + +def delete_inactive_timers(apps, schema_editor): + from core import models + + for timer in models.Timer.objects.filter(active=False): + timer.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0026_alter_feeding_end_alter_feeding_start_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="timer", + options={ + "default_permissions": ("view", "add", "change", "delete"), + "ordering": ["-start"], + "verbose_name": "Timer", + "verbose_name_plural": "Timers", + }, + ), + migrations.RemoveField( + model_name="timer", + name="duration", + ), + migrations.RemoveField( + model_name="timer", + name="end", + ), + migrations.RunPython( + delete_inactive_timers, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/core/models.py b/core/models.py index 0a5658c8..e6b77c25 100644 --- a/core/models.py +++ b/core/models.py @@ -559,12 +559,6 @@ class Timer(models.Model): start = models.DateTimeField( default=timezone.now, blank=False, verbose_name=_("Start time") ) - end = models.DateTimeField( - blank=True, editable=False, null=True, verbose_name=_("End time") - ) - duration = models.DurationField( - editable=False, null=True, verbose_name=_("Duration") - ) active = models.BooleanField(default=True, editable=False, verbose_name=_("Active")) user = models.ForeignKey( "auth.User", @@ -577,7 +571,7 @@ class Timer(models.Model): class Meta: default_permissions = ("view", "add", "change", "delete") - ordering = ["-active", "-start", "-end"] + ordering = ["-start"] verbose_name = _("Timer") verbose_name_plural = _("Timers") @@ -600,42 +594,24 @@ class Timer(models.Model): return self.user.get_full_name() return self.user.get_username() - @classmethod - def from_db(cls, db, field_names, values): - instance = super(Timer, cls).from_db(db, field_names, values) - if not instance.duration: - instance.duration = timezone.now() - instance.start - return instance + def duration(self): + return timezone.now() - self.start def restart(self): """Restart the timer.""" self.start = timezone.now() - self.end = None - self.duration = None - self.active = True self.save() - def stop(self, end=None): - """Stop the timer.""" - if not end: - end = timezone.now() - self.end = end - self.save() + def stop(self): + """Stop (delete) the timer.""" + self.delete() def save(self, *args, **kwargs): - self.active = self.end is None self.name = self.name or None - if self.start and self.end: - self.duration = self.end - self.start - else: - self.duration = None super(Timer, self).save(*args, **kwargs) def clean(self): validate_time(self.start, "start") - if self.end: - validate_time(self.end, "end") - validate_duration(self) class TummyTime(models.Model): diff --git a/core/static_src/js/timer.js b/core/static_src/js/timer.js index bb594ca0..d3b8f598 100644 --- a/core/static_src/js/timer.js +++ b/core/static_src/js/timer.js @@ -93,13 +93,7 @@ BabyBuddy.Timer = function ($) { timerElement.find('.timer-minutes').text(parseInt(duration[1])); timerElement.find('.timer-seconds').text(parseInt(duration[2])); lastUpdate = new Date() - - if (data['active']) { - runIntervalId = setInterval(Timer.tick, 1000); - } - else { - timerElement.addClass('timer-stopped'); - } + runIntervalId = setInterval(Timer.tick, 1000); } }); } diff --git a/core/templates/core/timer_confirm_delete_inactive.html b/core/templates/core/timer_confirm_delete_inactive.html deleted file mode 100644 index ad6bc4ba..00000000 --- a/core/templates/core/timer_confirm_delete_inactive.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'babybuddy/page.html' %} -{% load humanize i18n widget_tweaks %} - -{% block title %} - {% blocktrans %}Delete All Inactive Timers{% endblocktrans %} -{% endblock %} - -{% block breadcrumbs %} - - -{% endblock %} - -{% block content %} -
- {% csrf_token %} -

- {% blocktrans trimmed with number=timer_count|apnumber|intcomma count counter=timer_count %} - Are you sure you want to delete {{ number }} inactive timer? - {% plural %} - Are you sure you want to delete {{ number }} inactive timers? - {% endblocktrans %} -

- - {% trans "Cancel" %} -
-{% endblock %} \ No newline at end of file diff --git a/core/templates/core/timer_detail.html b/core/templates/core/timer_detail.html index fa373b50..e091d7f9 100644 --- a/core/templates/core/timer_detail.html +++ b/core/templates/core/timer_detail.html @@ -13,7 +13,7 @@

+ class="display-1"> {{ object.duration|hours }}h {{ object.duration|minutes }}m {{ object.duration|seconds }}s @@ -27,9 +27,6 @@

{% trans "Started" %} {{ object.start }} - {% if not object.active %} - / {% trans "Stopped" %} {{ object.end }} - {% endif %}

{% blocktrans trimmed with user=object.user_username %} @@ -80,14 +77,6 @@ - - {% if object.active %} -

- {% csrf_token %} - - -
- {% endif %} {% endif %}

@@ -95,9 +84,7 @@ {% endblock %} {% block javascript %} - {% if object.active %} - - {% endif %} + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/timer_list.html b/core/templates/core/timer_list.html index df383838..c9eb433e 100644 --- a/core/templates/core/timer_list.html +++ b/core/templates/core/timer_list.html @@ -62,11 +62,4 @@ {% include 'babybuddy/paginator.html' %} - - {% if object_list and perms.core.delete_timer %} - - {% trans "Delete Inactive Timers" %} - - {% endif %} - {% endblock %} \ No newline at end of file diff --git a/core/templates/core/timer_nav.html b/core/templates/core/timer_nav.html index d439fd8d..ae8109a6 100644 --- a/core/templates/core/timer_nav.html +++ b/core/templates/core/timer_nav.html @@ -49,7 +49,7 @@ {% endif %} {% if timers %} - + {% for timer in timers %} {{ timer.title_with_child }} diff --git a/core/templatetags/timers.py b/core/templatetags/timers.py index 15b48962..2dd48c35 100644 --- a/core/templatetags/timers.py +++ b/core/templatetags/timers.py @@ -8,15 +8,14 @@ register = template.Library() @register.inclusion_tag("core/timer_nav.html", takes_context=True) -def timer_nav(context, active=True): +def timer_nav(context): """ - Get a list of active Timer instances to include in the nav menu. + Get a list of Timer instances to include in the nav menu. :param context: Django's context data. - :param active: the state of Timers to filter. :returns: a dictionary with timers data. """ request = context["request"] or None - timers = Timer.objects.filter(active=active) + timers = Timer.objects.filter() children = Child.objects.all() perms = context["perms"] or None # The 'next' parameter is currently not used. diff --git a/core/tests/tests_forms.py b/core/tests/tests_forms.py index 1fed7a34..9466c588 100644 --- a/core/tests/tests_forms.py +++ b/core/tests/tests_forms.py @@ -122,27 +122,23 @@ class InitialValuesTestCase(FormsTestCaseBase): self.assertEqual(page.context["form"].initial["type"], f_three.type) self.assertEqual(page.context["form"].initial["method"], f_three.method) - def test_timer_set(self): + def test_timer_form_field_set(self): self.timer.stop() page = self.c.get("/sleep/add/") self.assertTrue("start" not in page.context["form"].initial) self.assertTrue("end" not in page.context["form"].initial) - page = self.c.get("/sleep/add/?timer={}".format(self.timer.id)) - self.assertEqual(page.context["form"].initial["start"], self.timer.start) - self.assertEqual(page.context["form"].initial["end"], self.timer.end) - def test_timer_stop_on_save(self): - end = timezone.localtime() + timer = models.Timer.objects.create( + user=self.user, start=timezone.localtime() - timezone.timedelta(minutes=30) + ) params = { "child": self.child.id, "start": self.localtime_string(self.timer.start), - "end": self.localtime_string(end), + "end": self.localtime_string(), } - page = self.c.post( - "/sleep/add/?timer={}".format(self.timer.id), params, follow=True - ) + page = self.c.post("/sleep/add/?timer={}".format(timer.id), params, follow=True) self.assertEqual(page.status_code, 200) self.timer.refresh_from_db() self.assertFalse(self.timer.active) diff --git a/core/tests/tests_models.py b/core/tests/tests_models.py index fd55f308..d413847a 100644 --- a/core/tests/tests_models.py +++ b/core/tests/tests_models.py @@ -270,11 +270,9 @@ class TimerTestCase(TestCase): ) self.user = get_user_model().objects.first() self.named = models.Timer.objects.create( - name="Named", end=timezone.localtime(), user=self.user, child=child - ) - self.unnamed = models.Timer.objects.create( - end=timezone.localtime(), user=self.user + name="Named", user=self.user, child=child ) + self.unnamed = models.Timer.objects.create(user=self.user) def test_timer_create(self): self.assertEqual(self.named, models.Timer.objects.get(name="Named")) @@ -302,19 +300,7 @@ class TimerTestCase(TestCase): def test_timer_restart(self): self.named.restart() - self.assertIsNone(self.named.end) - self.assertIsNone(self.named.duration) - self.assertTrue(self.named.active) - - def test_timer_stop(self): - stop_time = timezone.localtime() - self.unnamed.stop(end=stop_time) - self.assertEqual(self.unnamed.end, stop_time) - self.assertEqual( - self.unnamed.duration.seconds, - (self.unnamed.end - self.unnamed.start).seconds, - ) - self.assertFalse(self.unnamed.active) + self.assertGreaterEqual(timezone.localtime(), self.named.start) def test_timer_duration(self): timer = models.Timer.objects.create(user=get_user_model().objects.first()) @@ -322,9 +308,9 @@ class TimerTestCase(TestCase): timer.save() timer.refresh_from_db() - self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds) - timer.stop() - self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds) + self.assertEqual( + timer.duration().seconds, timezone.timedelta(minutes=30).seconds + ) class TummyTimeTestCase(TestCase): diff --git a/core/tests/tests_views.py b/core/tests/tests_views.py index 3a0ea1a0..53ad6bef 100644 --- a/core/tests/tests_views.py +++ b/core/tests/tests_views.py @@ -178,28 +178,11 @@ class ViewsTestCase(TestCase): page = self.c.get("/timers/{}/delete/".format(entry.id)) self.assertEqual(page.status_code, 200) - page = self.c.get("/timers/{}/stop/".format(entry.id)) - self.assertEqual(page.status_code, 405) - page = self.c.post("/timers/{}/stop/".format(entry.id), follow=True) - self.assertEqual(page.status_code, 200) - page = self.c.get("/timers/{}/restart/".format(entry.id)) self.assertEqual(page.status_code, 405) page = self.c.post("/timers/{}/restart/".format(entry.id), follow=True) self.assertEqual(page.status_code, 200) - page = self.c.get("/timers/delete-inactive/", follow=True) - self.assertEqual(page.status_code, 200) - messages = list(page.context["messages"]) - self.assertEqual(len(messages), 1) - self.assertEqual(str(messages[0]), "No inactive timers exist.") - - entry = models.Timer.objects.first() - entry.stop() - page = self.c.get("/timers/delete-inactive/") - self.assertEqual(page.status_code, 200) - self.assertEqual(page.context["timer_count"], 1) - def test_timeline_views(self): child = models.Child.objects.first() response = self.c.get("/timeline/") diff --git a/core/urls.py b/core/urls.py index c660942c..efbce751 100644 --- a/core/urls.py +++ b/core/urls.py @@ -72,15 +72,9 @@ urlpatterns = [ path("timers//", views.TimerDetail.as_view(), name="timer-detail"), path("timers//edit/", views.TimerUpdate.as_view(), name="timer-update"), path("timers//delete/", views.TimerDelete.as_view(), name="timer-delete"), - path("timers//stop/", views.TimerStop.as_view(), name="timer-stop"), path( "timers//restart/", views.TimerRestart.as_view(), name="timer-restart" ), - path( - "timers/delete-inactive/", - views.TimerDeleteInactive.as_view(), - name="timer-delete-inactive", - ), path("tummy-time/", views.TummyTimeList.as_view(), name="tummytime-list"), path("tummy-time/add/", views.TummyTimeAdd.as_view(), name="tummytime-add"), path( diff --git a/core/views.py b/core/views.py index 436fd8a1..9bac66d0 100644 --- a/core/views.py +++ b/core/views.py @@ -404,7 +404,7 @@ class TimerList(PermissionRequiredMixin, BabyBuddyFilterView): template_name = "core/timer_list.html" permission_required = ("core.view_timer",) paginate_by = 10 - filterset_fields = ("active", "user") + filterset_fields = ("user",) class TimerDetail(PermissionRequiredMixin, DetailView): @@ -477,55 +477,12 @@ class TimerRestart(PermissionRequiredMixin, RedirectView): return reverse("core:timer-detail", kwargs={"pk": kwargs["pk"]}) -class TimerStop(PermissionRequiredMixin, SuccessMessageMixin, RedirectView): - http_method_names = ["post"] - permission_required = ("core.change_timer",) - success_message = _("%(timer)s stopped.") - - def post(self, request, *args, **kwargs): - instance = models.Timer.objects.get(id=kwargs["pk"]) - instance.stop() - messages.success(request, "{} stopped.".format(instance)) - return super(TimerStop, self).get(request, *args, **kwargs) - - def get_redirect_url(self, *args, **kwargs): - return reverse("core:timer-detail", kwargs={"pk": kwargs["pk"]}) - - class TimerDelete(CoreDeleteView): model = models.Timer permission_required = ("core.delete_timer",) success_url = reverse_lazy("core:timer-list") -class TimerDeleteInactive(PermissionRequiredMixin, SuccessMessageMixin, FormView): - permission_required = ("core.delete_timer",) - form_class = Form - template_name = "core/timer_confirm_delete_inactive.html" - success_url = reverse_lazy("core:timer-list") - success_message = _("All inactive timers deleted.") - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["timer_count"] = self.get_instances().count() - return kwargs - - def get(self, request, *args, **kwargs): - # Redirect back to list if there are no inactive timers. - if self.get_instances().count() == 0: - messages.warning(request, _("No inactive timers exist.")) - return HttpResponseRedirect(self.success_url) - return super().get(request, *args, **kwargs) - - def form_valid(self, form): - self.get_instances().delete() - return super().form_valid(form) - - @staticmethod - def get_instances(): - return models.Timer.objects.filter(active=False) - - class TummyTimeList(PermissionRequiredMixin, BabyBuddyFilterView): model = models.TummyTime template_name = "core/tummytime_list.html" diff --git a/dashboard/templates/cards/timer_list.html b/dashboard/templates/cards/timer_list.html index 2fc3567d..eb6f8e63 100644 --- a/dashboard/templates/cards/timer_list.html +++ b/dashboard/templates/cards/timer_list.html @@ -3,16 +3,16 @@ {% block header %} - {% trans "Active Timers" %} + {% trans "Timers" %} {% endblock %} {% block title %} {% with instances|length as count %} {% blocktrans trimmed count counter=count %} - {{ count }} active timer + {{ count }} timer {% plural %} - {{ count }} active timers + {{ count }} timers {% endblocktrans %} {% endwith %} {% endblock %} diff --git a/dashboard/templatetags/cards.py b/dashboard/templatetags/cards.py index 17ef75ac..11e9f14b 100644 --- a/dashboard/templatetags/cards.py +++ b/dashboard/templatetags/cards.py @@ -707,10 +707,10 @@ def card_timer_list(context, child=None): if child: # Get active instances for the selected child _or_ None (no child). instances = models.Timer.objects.filter( - Q(active=True), Q(child=child) | Q(child=None) + Q(child=child) | Q(child=None) ).order_by("-start") else: - instances = models.Timer.objects.filter(active=True).order_by("-start") + instances = models.Timer.objects.order_by("-start") empty = len(instances) == 0 return { diff --git a/dashboard/tests/tests_templatetags.py b/dashboard/tests/tests_templatetags.py index 1c515b30..f26e96c6 100644 --- a/dashboard/tests/tests_templatetags.py +++ b/dashboard/tests/tests_templatetags.py @@ -323,7 +323,7 @@ class TemplateTagsTestCase(TestCase): data = cards.card_timer_list(self.context) self.assertIsInstance(data["instances"][0], models.Timer) - self.assertEqual(len(data["instances"]), 3) + self.assertEqual(len(data["instances"]), 4) data = cards.card_timer_list(self.context, child) self.assertIsInstance(data["instances"][0], models.Timer) diff --git a/docs/api.md b/docs/api.md index 26c0c142..266aafff 100644 --- a/docs/api.md +++ b/docs/api.md @@ -222,9 +222,7 @@ Note the timer `id` in the response: "child": 1, "name": null, "start": "2022-05-28T19:59:40.013914Z", - "end": null, "duration": null, - "active": true, "user": 1 } ``` @@ -252,25 +250,7 @@ Note that `child` and `start` match the timer values (and `end` is auto-populate } ``` -Also note that the timer has been stopped: - -```shell -curl --location --request GET '[...]/api/timers/5' \ ---header 'Authorization: Token [...]' -``` - -```json -{ - "id": 5, - "child": 1, - "name": null, - "start": "2022-05-28T19:59:40.013914Z", - "end": "2022-05-28T20:01:13.549099Z", - "duration": "00:01:33.535185", - "active": false, - "user": 1 -} -``` +Also note that the timer has been deleted. ### Response diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 4d0aebad..f113c317 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -19,7 +19,7 @@ an overview of all elements related to your child, including: - Last sleep is the start of the last nap/sleep, and includes the nap duration below. - Last feeding method is a quick view of how the baby was last fed. This is particularly useful for nursing mothers to remember which breast they started with on the previous feed. - Today's Feeding is a snapshot of the total numbers of daily feeds. -- Active timers let you know if you have a timer running. +- Timers let you know if you have a timer running. - Statistics is a snapshot of various statistics – these can be scrolled through or select Statistics from the menu bar to see more. - Today's Sleep lists the total number of hours slept for the day. - Today's Naps lists the number of naps taken that day in bold and the total nap time below. diff --git a/static/babybuddy/js/app.d20500d757a5.js b/static/babybuddy/js/app.d20500d757a5.js new file mode 100644 index 00000000..9c004f47 --- /dev/null +++ b/static/babybuddy/js/app.d20500d757a5.js @@ -0,0 +1,206 @@ +if (typeof jQuery === 'undefined') { + throw new Error('Baby Buddy requires jQuery.') +} + +/** + * Baby Buddy Namespace + * + * Default namespace for the Baby Buddy app. + * + * @type {{}} + */ +var BabyBuddy = function () { + return {}; +}(); + +/** + * Pull to refresh. + * + * @type {{init: BabyBuddy.PullToRefresh.init, onRefresh: BabyBuddy.PullToRefresh.onRefresh}} + */ +BabyBuddy.PullToRefresh = function(ptr) { + return { + init: function () { + ptr.init({ + mainElement: 'body', + onRefresh: this.onRefresh + }); + }, + + onRefresh: function() { + window.location.reload(); + } + }; +}(PullToRefresh); + +/** + * Fix for duplicate form submission from double pressing submit + */ +function preventDoubleSubmit() { + return false; +} +$('form').off("submit", preventDoubleSubmit); +$("form").on("submit", function() { + $(this).on("submit", preventDoubleSubmit); +}); + +/* Baby Buddy Timer + * + * Uses a supplied ID to run a timer. The element using the ID must have + * three children with the following classes: + * * timer-seconds + * * timer-minutes + * * timer-hours + */ +BabyBuddy.Timer = function ($) { + var runIntervalId = null; + var timerId = null; + var timerElement = null; + var lastUpdate = new Date(); + var hidden = null; + + var Timer = { + run: function(timer_id, element_id) { + timerId = timer_id; + timerElement = $('#' + element_id); + + if (timerElement.length === 0) { + console.error('BBTimer: Timer element not found.'); + return false; + } + + if (timerElement.find('.timer-seconds').length === 0 + || timerElement.find('.timer-minutes').length === 0 + || timerElement.find('.timer-hours').length === 0) { + console.error('BBTimer: Element does not contain expected children.'); + return false; + } + + runIntervalId = setInterval(this.tick, 1000); + + // If the page just came in to view, update the timer data with the + // current actual duration. This will (potentially) help mobile + // phones that lock with the timer page open. + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + } + else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + } + else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + } + window.addEventListener('focus', Timer.handleVisibilityChange, false); + }, + + handleVisibilityChange: function() { + if (!document[hidden] && (new Date()) - lastUpdate > 1) { + Timer.update(); + } + }, + + tick: function() { + var s = timerElement.find('.timer-seconds'); + var seconds = Number(s.text()); + if (seconds < 59) { + s.text(seconds + 1); + return; + } + else { + s.text(0); + } + + var m = timerElement.find('.timer-minutes'); + var minutes = Number(m.text()); + if (minutes < 59) { + m.text(minutes + 1); + return; + } + else { + m.text(0); + } + + var h = timerElement.find('.timer-hours'); + var hours = Number(h.text()); + h.text(hours + 1); + }, + + update: function() { + $.get('/api/timers/' + timerId + '/', function(data) { + if (data && 'duration' in data) { + clearInterval(runIntervalId); + var duration = data.duration.split(/[\s:.]/) + if (duration.length === 5) { + duration[0] = parseInt(duration[0]) * 24 + parseInt(duration[1]); + duration[1] = duration[2]; + duration[2] = duration[3]; + } + timerElement.find('.timer-hours').text(parseInt(duration[0])); + timerElement.find('.timer-minutes').text(parseInt(duration[1])); + timerElement.find('.timer-seconds').text(parseInt(duration[2])); + lastUpdate = new Date() + runIntervalId = setInterval(Timer.tick, 1000); + } + }); + } + }; + + return Timer; +}(jQuery); + +/* Baby Buddy Dashboard + * + * Provides a "watch" function to update the dashboard at one minute intervals + * and/or on visibility state changes. + */ +BabyBuddy.Dashboard = function ($) { + var runIntervalId = null; + var dashboardElement = null; + var hidden = null; + + var Dashboard = { + watch: function(element_id, refresh_rate) { + dashboardElement = $('#' + element_id); + + if (dashboardElement.length == 0) { + console.error('Baby Buddy: Dashboard element not found.'); + return false; + } + + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + } + else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + } + else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + } + + if (typeof window.addEventListener === "undefined" || typeof document.hidden === "undefined") { + if (refresh_rate) { + runIntervalId = setInterval(this.update, refresh_rate); + } + } + else { + window.addEventListener('focus', Dashboard.handleVisibilityChange, false); + if (refresh_rate) { + runIntervalId = setInterval(Dashboard.handleVisibilityChange, refresh_rate); + } + } + }, + + handleVisibilityChange: function() { + if (!document[hidden]) { + Dashboard.update(); + } + }, + + update: function() { + // TODO: Someday maybe update in place? + location.reload(); + } + }; + + return Dashboard; +}(jQuery); diff --git a/static/babybuddy/js/app.d20500d757a5.js.gz b/static/babybuddy/js/app.d20500d757a5.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..3f61214c2354574f6f92e973a5a0f8b493b97872 GIT binary patch literal 1554 zcmV+t2JQJDiwFP!00002|Ls^ybK5o$zUx;Yswbopg|gk|P^)e`uH30-I!W3%y`-KD zM1Uq@l3)o?ipJ4@?*b%9@FgjZdn->Qu)Em(uurl8xpI?I9-xnZ zZlN*)#bll(=zcpNZQaSd(&uO+R-QBP3lXer5rx61|+G%1W z@@33~P{;%sj}hVdGNLF2Zo!h=MXeZ=^cRBqJRzd77PBOmw7C}YJQJ;O&~`Ke_{adHVjPUPN+iuzIJzQ0mVjr> zt6eR2XuWj$&L&tO-=#!(&$&)&3wSk-s0czv2v~3Us&%7k?bx85>l*A?+giDW$%Ih%_Pfb!fJOK~QPFbuIyO{JSYSIX*sayUA>Zt^ys)DZZuX zql$VTN-tCvl`B?^uBRw3qC!z=U=V!bDhssb`FWNptFbS096?Ubip&nVh6&(;GV7*X z0tv%Ww3(ogMk$KM0b_O7j!|?7COczYy-{`ew`MeFB#_ zt{Rzf-*r}TJ}%zIZSd_c3YXJ+UMzllz=?+IVP~%C@{Y-eoNIILJiDydn2<|VkbY+Z zZjxrI68gDtrbWK+LY$C@{>21?Yh`lt3oP8yY2nNR?Lt4#j^05*@WdGZzA)wnJ$;JY zI(<*j3p=y_h)()|D3mVz+?@>BZ!4=pYKLm7E(nv&cPF`dm$BFa_E&kl1aKjO=_lwz zTC~c3L;C}I`F)>dC95HO4)zaan!PSJDE)Y{Ge0St;+~x{f8#NGNKM>_|LEJRacHqLd+AKfZxOQ=AD3n!48V1lruw?*Oju=E==~Ee**h zN^i%{}yH~HN1Lhpkr40x@d4SNmeH;*aIw*8t2I!^T)pZs1 zq|tWmMt|Ek@Y+MCmZ)an;(tna@LOOJE;-IfIsCoJ_=*ud0FKvK`r){qrJ%F3dOBh_ z5p-G5Z&2y^)L>&5Fc^p?kz;0=oLW=Ty2fkrNkuup$#@Er0#*Di0B>WvcjFF#kJtB4o? E0QR~0hyVZp literal 0 HcmV?d00001 diff --git a/static/babybuddy/js/app.js b/static/babybuddy/js/app.js index 23143cbc..9c004f47 100644 --- a/static/babybuddy/js/app.js +++ b/static/babybuddy/js/app.js @@ -1 +1,206 @@ -if("undefined"==typeof jQuery)throw new Error("Baby Buddy requires jQuery.");var BabyBuddy={};function preventDoubleSubmit(){return!1}BabyBuddy.PullToRefresh=function(e){return{init:function(){e.init({mainElement:"body",onRefresh:this.onRefresh})},onRefresh:function(){window.location.reload()}}}(PullToRefresh),$("form").off("submit",preventDoubleSubmit),$("form").on("submit",function(){$(this).on("submit",preventDoubleSubmit)}),BabyBuddy.Timer=function(e){var n=null,t=null,i=null,d=new Date,r=null,o={run:function(d,u){return t=d,0===(i=e("#"+u)).length?(console.error("BBTimer: Timer element not found."),!1):0===i.find(".timer-seconds").length||0===i.find(".timer-minutes").length||0===i.find(".timer-hours").length?(console.error("BBTimer: Element does not contain expected children."),!1):(n=setInterval(this.tick,1e3),void 0!==document.hidden?r="hidden":void 0!==document.msHidden?r="msHidden":void 0!==document.webkitHidden&&(r="webkitHidden"),void window.addEventListener("focus",o.handleVisibilityChange,!1))},handleVisibilityChange:function(){!document[r]&&new Date-d>1&&o.update()},tick:function(){var e=i.find(".timer-seconds"),n=Number(e.text());if(n<59)e.text(n+1);else{e.text(0);var t=i.find(".timer-minutes"),d=Number(t.text());if(d<59)t.text(d+1);else{t.text(0);var r=i.find(".timer-hours"),o=Number(r.text());r.text(o+1)}}},update:function(){e.get("/api/timers/"+t+"/",function(e){if(e&&"duration"in e){clearInterval(n);var t=e.duration.split(/[\s:.]/);5===t.length&&(t[0]=24*parseInt(t[0])+parseInt(t[1]),t[1]=t[2],t[2]=t[3]),i.find(".timer-hours").text(parseInt(t[0])),i.find(".timer-minutes").text(parseInt(t[1])),i.find(".timer-seconds").text(parseInt(t[2])),d=new Date,e.active?n=setInterval(o.tick,1e3):i.addClass("timer-stopped")}})}};return o}(jQuery),BabyBuddy.Dashboard=function(e){var n=null,t={watch:function(i,d){if(0==e("#"+i).length)return console.error("Baby Buddy: Dashboard element not found."),!1;void 0!==document.hidden?n="hidden":void 0!==document.msHidden?n="msHidden":void 0!==document.webkitHidden&&(n="webkitHidden"),void 0===window.addEventListener||void 0===document.hidden?d&&setInterval(this.update,d):(window.addEventListener("focus",t.handleVisibilityChange,!1),d&&setInterval(t.handleVisibilityChange,d))},handleVisibilityChange:function(){document[n]||t.update()},update:function(){location.reload()}};return t}(jQuery); \ No newline at end of file +if (typeof jQuery === 'undefined') { + throw new Error('Baby Buddy requires jQuery.') +} + +/** + * Baby Buddy Namespace + * + * Default namespace for the Baby Buddy app. + * + * @type {{}} + */ +var BabyBuddy = function () { + return {}; +}(); + +/** + * Pull to refresh. + * + * @type {{init: BabyBuddy.PullToRefresh.init, onRefresh: BabyBuddy.PullToRefresh.onRefresh}} + */ +BabyBuddy.PullToRefresh = function(ptr) { + return { + init: function () { + ptr.init({ + mainElement: 'body', + onRefresh: this.onRefresh + }); + }, + + onRefresh: function() { + window.location.reload(); + } + }; +}(PullToRefresh); + +/** + * Fix for duplicate form submission from double pressing submit + */ +function preventDoubleSubmit() { + return false; +} +$('form').off("submit", preventDoubleSubmit); +$("form").on("submit", function() { + $(this).on("submit", preventDoubleSubmit); +}); + +/* Baby Buddy Timer + * + * Uses a supplied ID to run a timer. The element using the ID must have + * three children with the following classes: + * * timer-seconds + * * timer-minutes + * * timer-hours + */ +BabyBuddy.Timer = function ($) { + var runIntervalId = null; + var timerId = null; + var timerElement = null; + var lastUpdate = new Date(); + var hidden = null; + + var Timer = { + run: function(timer_id, element_id) { + timerId = timer_id; + timerElement = $('#' + element_id); + + if (timerElement.length === 0) { + console.error('BBTimer: Timer element not found.'); + return false; + } + + if (timerElement.find('.timer-seconds').length === 0 + || timerElement.find('.timer-minutes').length === 0 + || timerElement.find('.timer-hours').length === 0) { + console.error('BBTimer: Element does not contain expected children.'); + return false; + } + + runIntervalId = setInterval(this.tick, 1000); + + // If the page just came in to view, update the timer data with the + // current actual duration. This will (potentially) help mobile + // phones that lock with the timer page open. + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + } + else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + } + else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + } + window.addEventListener('focus', Timer.handleVisibilityChange, false); + }, + + handleVisibilityChange: function() { + if (!document[hidden] && (new Date()) - lastUpdate > 1) { + Timer.update(); + } + }, + + tick: function() { + var s = timerElement.find('.timer-seconds'); + var seconds = Number(s.text()); + if (seconds < 59) { + s.text(seconds + 1); + return; + } + else { + s.text(0); + } + + var m = timerElement.find('.timer-minutes'); + var minutes = Number(m.text()); + if (minutes < 59) { + m.text(minutes + 1); + return; + } + else { + m.text(0); + } + + var h = timerElement.find('.timer-hours'); + var hours = Number(h.text()); + h.text(hours + 1); + }, + + update: function() { + $.get('/api/timers/' + timerId + '/', function(data) { + if (data && 'duration' in data) { + clearInterval(runIntervalId); + var duration = data.duration.split(/[\s:.]/) + if (duration.length === 5) { + duration[0] = parseInt(duration[0]) * 24 + parseInt(duration[1]); + duration[1] = duration[2]; + duration[2] = duration[3]; + } + timerElement.find('.timer-hours').text(parseInt(duration[0])); + timerElement.find('.timer-minutes').text(parseInt(duration[1])); + timerElement.find('.timer-seconds').text(parseInt(duration[2])); + lastUpdate = new Date() + runIntervalId = setInterval(Timer.tick, 1000); + } + }); + } + }; + + return Timer; +}(jQuery); + +/* Baby Buddy Dashboard + * + * Provides a "watch" function to update the dashboard at one minute intervals + * and/or on visibility state changes. + */ +BabyBuddy.Dashboard = function ($) { + var runIntervalId = null; + var dashboardElement = null; + var hidden = null; + + var Dashboard = { + watch: function(element_id, refresh_rate) { + dashboardElement = $('#' + element_id); + + if (dashboardElement.length == 0) { + console.error('Baby Buddy: Dashboard element not found.'); + return false; + } + + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + } + else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + } + else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + } + + if (typeof window.addEventListener === "undefined" || typeof document.hidden === "undefined") { + if (refresh_rate) { + runIntervalId = setInterval(this.update, refresh_rate); + } + } + else { + window.addEventListener('focus', Dashboard.handleVisibilityChange, false); + if (refresh_rate) { + runIntervalId = setInterval(Dashboard.handleVisibilityChange, refresh_rate); + } + } + }, + + handleVisibilityChange: function() { + if (!document[hidden]) { + Dashboard.update(); + } + }, + + update: function() { + // TODO: Someday maybe update in place? + location.reload(); + } + }; + + return Dashboard; +}(jQuery); diff --git a/static/babybuddy/js/app.js.gz b/static/babybuddy/js/app.js.gz index a26b6700df78a24affbe7da7a5290dd844146673..3f61214c2354574f6f92e973a5a0f8b493b97872 100644 GIT binary patch literal 1554 zcmV+t2JQJDiwFP!00002|Ls^ybK5o$zUx;Yswbopg|gk|P^)e`uH30-I!W3%y`-KD zM1Uq@l3)o?ipJ4@?*b%9@FgjZdn->Qu)Em(uurl8xpI?I9-xnZ zZlN*)#bll(=zcpNZQaSd(&uO+R-QBP3lXer5rx61|+G%1W z@@33~P{;%sj}hVdGNLF2Zo!h=MXeZ=^cRBqJRzd77PBOmw7C}YJQJ;O&~`Ke_{adHVjPUPN+iuzIJzQ0mVjr> zt6eR2XuWj$&L&tO-=#!(&$&)&3wSk-s0czv2v~3Us&%7k?bx85>l*A?+giDW$%Ih%_Pfb!fJOK~QPFbuIyO{JSYSIX*sayUA>Zt^ys)DZZuX zql$VTN-tCvl`B?^uBRw3qC!z=U=V!bDhssb`FWNptFbS096?Ubip&nVh6&(;GV7*X z0tv%Ww3(ogMk$KM0b_O7j!|?7COczYy-{`ew`MeFB#_ zt{Rzf-*r}TJ}%zIZSd_c3YXJ+UMzllz=?+IVP~%C@{Y-eoNIILJiDydn2<|VkbY+Z zZjxrI68gDtrbWK+LY$C@{>21?Yh`lt3oP8yY2nNR?Lt4#j^05*@WdGZzA)wnJ$;JY zI(<*j3p=y_h)()|D3mVz+?@>BZ!4=pYKLm7E(nv&cPF`dm$BFa_E&kl1aKjO=_lwz zTC~c3L;C}I`F)>dC95HO4)zaan!PSJDE)Y{Ge0St;+~x{f8#NGNKM>_|LEJRacHqLd+AKfZxOQ=AD3n!48V1lruw?*Oju=E==~Ee**h zN^i%{}yH~HN1Lhpkr40x@d4SNmeH;*aIw*8t2I!^T)pZs1 zq|tWmMt|Ek@Y+MCmZ)an;(tna@LOOJE;-IfIsCoJ_=*ud0FKvK`r){qrJ%F3dOBh_ z5p-G5Z&2y^)L>&5Fc^p?kz;0=oLW=Ty2fkrNkuup$#@Er0#*Di0B>WvcjFF#kJtB4o? E0QR~0hyVZp literal 923 zcmV;M17!RkiwFP!00002|CLrUY>hsHK)XD33H`E5+U4n1!f-o{n3NnGPkg1 z6KU8)kJcJX$$d84M)zLIZDipeuPl@emJ>EvXEq9m3Ltie$=vJQDWjv6g*9mRVEn9r zKm2T|9A!HT&RcyoK6D@P556cqncrcK%nQ+ePoV|xlvXb7;%o;zMCfjrDg9W$5@$)t z%*ZWCjBa>Rw@@YT9uMr$d)MRIC@sx~7bed_gj*;~CMi1{4)i23mb{{5ZtRjUZsu6R zvRV>JF0AtetB=s$ysv0T@bvX1rUOfkmHMQXU{5L(%CCjSL=#uvRDF{oY~e%ZAhC7C zh@JKNxOSQNw!)DUGPw~#P$htp@5r@hj2EDv-QtesMwg}l4vik}t2EN6xpbyUvPgmvGMfb25fEELj^Rqnj%sq{)3?<_jey z=sPP&JtFB#%(8snq1y2!+y-W!m8;*!F-3GgLYnCt$!1bM2D$vIN(UNjP+aay>;b;W zv@GCHRjQdPl-s_;z%zv9VH;e2+3$&~HlZgwjpJ5FujS9B%v(*?}8X% zaL*Ghe)G#2CJNla7c>JlQFE$)dix`5BKmsFCQy{HYXUd5!MH(ttsIeScTGmUrpI>0)F6(D^tN&v z5`@Thc>$U26sOxY18;G>T;Wd9(bHdL%BLeXd5az48u6o0+|$ie-2U+WDzhaZQ59g< zy~pvCCE+UE)9o|~7aYC8;6Pth%RQxgfju_rIeL5neeC^n^ld=*{0ex61zW@2X(yUv z8>T9#|6P%lB_$2LGpiLIXShH3n>4;=4zxMedM^5qm5Z6ltQ>aI-6nH+@0?JHtTqpw zsP;$IS`TYxIya#1&`hIlz9Fxi47!T`CRfq_%T@GcR|zIN;4J%nhd57A#_