Remove "inactive" timer concept -- delete on complete

Closes #109
This commit is contained in:
Christopher C. Wells 2022-08-14 13:55:57 -07:00 committed by Christopher C. Wells
parent a2e203a3f8
commit 7b7f17fde6
30 changed files with 502 additions and 275 deletions

View File

@ -99,7 +99,7 @@ class TemperatureFilter(TimeFieldFilter, TagsFieldFilter):
class TimerFilter(StartEndFieldFilter): class TimerFilter(StartEndFieldFilter):
class Meta(StartEndFieldFilter.Meta): class Meta(StartEndFieldFilter.Meta):
model = models.Timer model = models.Timer
fields = sorted(StartEndFieldFilter.Meta.fields + ["active", "user"]) fields = sorted(StartEndFieldFilter.Meta.fields + ["user"])
class TummyTimeFilter(StartEndFieldFilter, TagsFieldFilter): class TummyTimeFilter(StartEndFieldFilter, TagsFieldFilter):

View File

@ -77,16 +77,12 @@ class CoreModelWithDurationSerializer(CoreModelSerializer):
timer = attrs["timer"] timer = attrs["timer"]
attrs.pop("timer") attrs.pop("timer")
if timer.end:
end = timer.end
else:
end = timezone.now()
if timer.child: if timer.child:
attrs["child"] = timer.child attrs["child"] = timer.child
# Overwrites values provided directly! # Overwrites values provided directly!
attrs["start"] = timer.start attrs["start"] = timer.start
attrs["end"] = end attrs["end"] = timezone.now()
# The "child", "start", and "end" field should all be set at this # The "child", "start", and "end" field should all be set at this
# point. If one is not, model validation will fail because they are # 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. # Only actually stop the timer if all validation passed.
if timer: if timer:
timer.stop(attrs["end"]) timer.stop()
return attrs return attrs
@ -232,10 +228,11 @@ class TimerSerializer(CoreModelSerializer):
queryset=get_user_model().objects.all(), queryset=get_user_model().objects.all(),
required=False, required=False,
) )
duration = serializers.DurationField(read_only=True, required=False)
class Meta: class Meta:
model = models.Timer model = models.Timer
fields = ("id", "child", "name", "start", "end", "duration", "active", "user") fields = ("id", "child", "name", "start", "duration", "user")
def validate(self, attrs): def validate(self, attrs):
attrs = super(TimerSerializer, self).validate(attrs) attrs = super(TimerSerializer, self).validate(attrs)

View File

@ -47,7 +47,6 @@ class TestBase:
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
timer.refresh_from_db() timer.refresh_from_db()
self.assertTrue(timer.active)
child = models.Child.objects.first() child = models.Child.objects.first()
self.timer_test_data["child"] = child.id self.timer_test_data["child"] = child.id
@ -55,11 +54,9 @@ class TestBase:
self.endpoint, self.timer_test_data, format="json" self.endpoint, self.timer_test_data, format="json"
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) 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"]) obj = self.model.objects.get(pk=response.data["id"])
self.assertEqual(obj.start, start) self.assertEqual(obj.start, start)
self.assertEqual(obj.end, timer.end) self.assertIsNotNone(obj.end)
def test_post_with_timer_with_child(self): def test_post_with_timer_with_child(self):
if not self.timer_test_data: if not self.timer_test_data:
@ -73,12 +70,10 @@ class TestBase:
self.endpoint, self.timer_test_data, format="json" self.endpoint, self.timer_test_data, format="json"
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) 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"]) 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.start, start)
self.assertEqual(obj.end, timer.end) self.assertIsNotNone(obj.end)
class BMIAPITestCase(TestBase.BabyBuddyAPITestCaseBase): class BMIAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
@ -703,19 +698,7 @@ class TimerAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
def test_get(self): def test_get(self):
response = self.client.get(self.endpoint) response = self.client.get(self.endpoint)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(response.data["results"][0]["id"], 1)
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,
},
)
def test_post(self): def test_post(self):
data = {"name": "New fake timer", "user": 1} 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.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) endpoint = "{}{}/".format(self.endpoint, 1)
response = self.client.get(endpoint) response = self.client.get(endpoint)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["active"])
response = self.client.patch(f"{endpoint}restart/") response = self.client.patch(f"{endpoint}restart/")
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data["active"])
# Restart twice is allowed # Restart twice is allowed
response = self.client.patch(f"{endpoint}restart/") response = self.client.patch(f"{endpoint}restart/")
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): class TummyTimeAPITestCase(TestBase.BabyBuddyAPITestCaseBase):

View File

@ -118,12 +118,6 @@ class TimerViewSet(viewsets.ModelViewSet):
ordering_fields = ("duration", "end", "start") ordering_fields = ("duration", "end", "start")
ordering = "-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"]) @action(detail=True, methods=["patch"])
def restart(self, request, pk=None): def restart(self, request, pk=None):
timer = self.get_object() timer = self.get_object()

View File

@ -393,9 +393,6 @@
{ {
"name": "Fake timer", "name": "Fake timer",
"start": "2017-11-18T04:30:00Z", "start": "2017-11-18T04:30:00Z",
"end": "2017-11-18T05:30:00Z",
"duration": "01:00:00",
"active": false,
"user": 1 "user": 1
} }
}, },

View File

@ -216,8 +216,8 @@ class TemperatureAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
@admin.register(models.Timer) @admin.register(models.Timer)
class TimerAdmin(admin.ModelAdmin): class TimerAdmin(admin.ModelAdmin):
list_display = ("name", "child", "start", "end", "duration", "active", "user") list_display = ("name", "child", "start", "duration", "user")
list_filter = ("child", "active", "user") list_filter = ("child", "user")
search_fields = ("child__first_name", "child__last_name", "name", "user") search_fields = ("child__first_name", "child__last_name", "name", "user")

View File

@ -44,7 +44,7 @@ def set_initial_values(kwargs, form_type):
if timer_id: if timer_id:
timer = models.Timer.objects.get(id=timer_id) timer = models.Timer.objects.get(id=timer_id)
kwargs["initial"].update( 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. # 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) instance = super(CoreModelForm, self).save(commit=False)
if self.timer_id: if self.timer_id:
timer = models.Timer.objects.get(id=self.timer_id) timer = models.Timer.objects.get(id=self.timer_id)
timer.stop(instance.end) timer.stop()
if commit: if commit:
instance.save() instance.save()
self.save_m2m() self.save_m2m()

View File

@ -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
),
]

View File

@ -559,12 +559,6 @@ class Timer(models.Model):
start = models.DateTimeField( start = models.DateTimeField(
default=timezone.now, blank=False, verbose_name=_("Start time") 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")) active = models.BooleanField(default=True, editable=False, verbose_name=_("Active"))
user = models.ForeignKey( user = models.ForeignKey(
"auth.User", "auth.User",
@ -577,7 +571,7 @@ class Timer(models.Model):
class Meta: class Meta:
default_permissions = ("view", "add", "change", "delete") default_permissions = ("view", "add", "change", "delete")
ordering = ["-active", "-start", "-end"] ordering = ["-start"]
verbose_name = _("Timer") verbose_name = _("Timer")
verbose_name_plural = _("Timers") verbose_name_plural = _("Timers")
@ -600,42 +594,24 @@ class Timer(models.Model):
return self.user.get_full_name() return self.user.get_full_name()
return self.user.get_username() return self.user.get_username()
@classmethod def duration(self):
def from_db(cls, db, field_names, values): return timezone.now() - self.start
instance = super(Timer, cls).from_db(db, field_names, values)
if not instance.duration:
instance.duration = timezone.now() - instance.start
return instance
def restart(self): def restart(self):
"""Restart the timer.""" """Restart the timer."""
self.start = timezone.now() self.start = timezone.now()
self.end = None
self.duration = None
self.active = True
self.save() self.save()
def stop(self, end=None): def stop(self):
"""Stop the timer.""" """Stop (delete) the timer."""
if not end: self.delete()
end = timezone.now()
self.end = end
self.save()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.active = self.end is None
self.name = self.name or 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) super(Timer, self).save(*args, **kwargs)
def clean(self): def clean(self):
validate_time(self.start, "start") validate_time(self.start, "start")
if self.end:
validate_time(self.end, "end")
validate_duration(self)
class TummyTime(models.Model): class TummyTime(models.Model):

View File

@ -93,13 +93,7 @@ BabyBuddy.Timer = function ($) {
timerElement.find('.timer-minutes').text(parseInt(duration[1])); timerElement.find('.timer-minutes').text(parseInt(duration[1]));
timerElement.find('.timer-seconds').text(parseInt(duration[2])); timerElement.find('.timer-seconds').text(parseInt(duration[2]));
lastUpdate = new Date() lastUpdate = new Date()
runIntervalId = setInterval(Timer.tick, 1000);
if (data['active']) {
runIntervalId = setInterval(Timer.tick, 1000);
}
else {
timerElement.addClass('timer-stopped');
}
} }
}); });
} }

View File

@ -1,26 +0,0 @@
{% extends 'babybuddy/page.html' %}
{% load humanize i18n widget_tweaks %}
{% block title %}
{% blocktrans %}Delete All Inactive Timers{% endblocktrans %}
{% endblock %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'core:timer-list' %}">{% trans "Timers" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Delete Inactive" %}</li>
{% endblock %}
{% block content %}
<form role="form" method="post">
{% csrf_token %}
<h1>
{% 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 %}
</h1>
<input type="submit" value="{% trans "Delete" %}" class="btn btn-danger" />
<a href={% url "babybuddy:root-router" %} class="btn btn-default">{% trans "Cancel" %}</a>
</form>
{% endblock %}

View File

@ -13,7 +13,7 @@
<div class="p-5 mb-4 bg-dark rounded-3 text-center"> <div class="p-5 mb-4 bg-dark rounded-3 text-center">
<div class="container-fluid py-1"> <div class="container-fluid py-1">
<h1 id="timer-status" <h1 id="timer-status"
class="display-1 {% if not object.active %} timer-stopped{% endif %}"> class="display-1">
<span class="timer-hours">{{ object.duration|hours }}</span>h <span class="timer-hours">{{ object.duration|hours }}</span>h
<span class="timer-minutes">{{ object.duration|minutes }}</span>m <span class="timer-minutes">{{ object.duration|minutes }}</span>m
<span class="timer-seconds">{{ object.duration|seconds }}</span>s <span class="timer-seconds">{{ object.duration|seconds }}</span>s
@ -27,9 +27,6 @@
<p class="lead text-secondary"> <p class="lead text-secondary">
{% trans "Started" %} {{ object.start }} {% trans "Started" %} {{ object.start }}
{% if not object.active %}
/ {% trans "Stopped" %} {{ object.end }}
{% endif %}
</p> </p>
<p class="text-muted"> <p class="text-muted">
{% blocktrans trimmed with user=object.user_username %} {% blocktrans trimmed with user=object.user_username %}
@ -80,14 +77,6 @@
<label class="visually-hidden">{% trans "Restart timer" %}</label> <label class="visually-hidden">{% trans "Restart timer" %}</label>
<button type="submit" class="btn btn-lg btn-secondary"><i class="icon-refresh" aria-hidden="true"></i></button> <button type="submit" class="btn btn-lg btn-secondary"><i class="icon-refresh" aria-hidden="true"></i></button>
</form> </form>
{% if object.active %}
<form action="{% url 'core:timer-stop' timer.id %}" role="form" method="post" class="d-inline">
{% csrf_token %}
<label class="visually-hidden">{% trans "Delete timer" %}</label>
<button type="submit" class="btn btn-lg btn-warning"><i class="icon-stop" aria-hidden="true"></i></button>
</form>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -95,9 +84,7 @@
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}
{% if object.active %} <script type="application/javascript">
<script type="application/javascript"> BabyBuddy.Timer.run({{ timer.id }}, 'timer-status');
BabyBuddy.Timer.run({{ timer.id }}, 'timer-status'); </script>
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -62,11 +62,4 @@
</table> </table>
</div> </div>
{% include 'babybuddy/paginator.html' %} {% include 'babybuddy/paginator.html' %}
{% if object_list and perms.core.delete_timer %}
<a href="{% url 'core:timer-delete-inactive' %}" class="btn btn-sm btn-danger">
<i class="icon-delete" aria-hidden="true"></i> {% trans "Delete Inactive Timers" %}
</a>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -49,7 +49,7 @@
{% endif %} {% endif %}
{% if timers %} {% if timers %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<h6 class="dropdown-header">{% trans "Active Timers" %}</h6> <h6 class="dropdown-header">{% trans "Timers" %}</h6>
{% for timer in timers %} {% for timer in timers %}
<a class="dropdown-item" href="{% url 'core:timer-detail' timer.id %}"> <a class="dropdown-item" href="{% url 'core:timer-detail' timer.id %}">
{{ timer.title_with_child }} {{ timer.title_with_child }}

View File

@ -8,15 +8,14 @@ register = template.Library()
@register.inclusion_tag("core/timer_nav.html", takes_context=True) @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 context: Django's context data.
:param active: the state of Timers to filter.
:returns: a dictionary with timers data. :returns: a dictionary with timers data.
""" """
request = context["request"] or None request = context["request"] or None
timers = Timer.objects.filter(active=active) timers = Timer.objects.filter()
children = Child.objects.all() children = Child.objects.all()
perms = context["perms"] or None perms = context["perms"] or None
# The 'next' parameter is currently not used. # The 'next' parameter is currently not used.

View File

@ -122,27 +122,23 @@ class InitialValuesTestCase(FormsTestCaseBase):
self.assertEqual(page.context["form"].initial["type"], f_three.type) self.assertEqual(page.context["form"].initial["type"], f_three.type)
self.assertEqual(page.context["form"].initial["method"], f_three.method) 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() self.timer.stop()
page = self.c.get("/sleep/add/") page = self.c.get("/sleep/add/")
self.assertTrue("start" not in page.context["form"].initial) self.assertTrue("start" not in page.context["form"].initial)
self.assertTrue("end" 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): 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 = { params = {
"child": self.child.id, "child": self.child.id,
"start": self.localtime_string(self.timer.start), "start": self.localtime_string(self.timer.start),
"end": self.localtime_string(end), "end": self.localtime_string(),
} }
page = self.c.post( page = self.c.post("/sleep/add/?timer={}".format(timer.id), params, follow=True)
"/sleep/add/?timer={}".format(self.timer.id), params, follow=True
)
self.assertEqual(page.status_code, 200) self.assertEqual(page.status_code, 200)
self.timer.refresh_from_db() self.timer.refresh_from_db()
self.assertFalse(self.timer.active) self.assertFalse(self.timer.active)

View File

@ -270,11 +270,9 @@ class TimerTestCase(TestCase):
) )
self.user = get_user_model().objects.first() self.user = get_user_model().objects.first()
self.named = models.Timer.objects.create( self.named = models.Timer.objects.create(
name="Named", end=timezone.localtime(), user=self.user, child=child name="Named", user=self.user, child=child
)
self.unnamed = models.Timer.objects.create(
end=timezone.localtime(), user=self.user
) )
self.unnamed = models.Timer.objects.create(user=self.user)
def test_timer_create(self): def test_timer_create(self):
self.assertEqual(self.named, models.Timer.objects.get(name="Named")) self.assertEqual(self.named, models.Timer.objects.get(name="Named"))
@ -302,19 +300,7 @@ class TimerTestCase(TestCase):
def test_timer_restart(self): def test_timer_restart(self):
self.named.restart() self.named.restart()
self.assertIsNone(self.named.end) self.assertGreaterEqual(timezone.localtime(), self.named.start)
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)
def test_timer_duration(self): def test_timer_duration(self):
timer = models.Timer.objects.create(user=get_user_model().objects.first()) timer = models.Timer.objects.create(user=get_user_model().objects.first())
@ -322,9 +308,9 @@ class TimerTestCase(TestCase):
timer.save() timer.save()
timer.refresh_from_db() timer.refresh_from_db()
self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds) self.assertEqual(
timer.stop() timer.duration().seconds, timezone.timedelta(minutes=30).seconds
self.assertEqual(timer.duration.seconds, timezone.timedelta(minutes=30).seconds) )
class TummyTimeTestCase(TestCase): class TummyTimeTestCase(TestCase):

View File

@ -178,28 +178,11 @@ class ViewsTestCase(TestCase):
page = self.c.get("/timers/{}/delete/".format(entry.id)) page = self.c.get("/timers/{}/delete/".format(entry.id))
self.assertEqual(page.status_code, 200) 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)) page = self.c.get("/timers/{}/restart/".format(entry.id))
self.assertEqual(page.status_code, 405) self.assertEqual(page.status_code, 405)
page = self.c.post("/timers/{}/restart/".format(entry.id), follow=True) page = self.c.post("/timers/{}/restart/".format(entry.id), follow=True)
self.assertEqual(page.status_code, 200) 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): def test_timeline_views(self):
child = models.Child.objects.first() child = models.Child.objects.first()
response = self.c.get("/timeline/") response = self.c.get("/timeline/")

View File

@ -72,15 +72,9 @@ urlpatterns = [
path("timers/<int:pk>/", views.TimerDetail.as_view(), name="timer-detail"), path("timers/<int:pk>/", views.TimerDetail.as_view(), name="timer-detail"),
path("timers/<int:pk>/edit/", views.TimerUpdate.as_view(), name="timer-update"), path("timers/<int:pk>/edit/", views.TimerUpdate.as_view(), name="timer-update"),
path("timers/<int:pk>/delete/", views.TimerDelete.as_view(), name="timer-delete"), path("timers/<int:pk>/delete/", views.TimerDelete.as_view(), name="timer-delete"),
path("timers/<int:pk>/stop/", views.TimerStop.as_view(), name="timer-stop"),
path( path(
"timers/<int:pk>/restart/", views.TimerRestart.as_view(), name="timer-restart" "timers/<int:pk>/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/", views.TummyTimeList.as_view(), name="tummytime-list"),
path("tummy-time/add/", views.TummyTimeAdd.as_view(), name="tummytime-add"), path("tummy-time/add/", views.TummyTimeAdd.as_view(), name="tummytime-add"),
path( path(

View File

@ -404,7 +404,7 @@ class TimerList(PermissionRequiredMixin, BabyBuddyFilterView):
template_name = "core/timer_list.html" template_name = "core/timer_list.html"
permission_required = ("core.view_timer",) permission_required = ("core.view_timer",)
paginate_by = 10 paginate_by = 10
filterset_fields = ("active", "user") filterset_fields = ("user",)
class TimerDetail(PermissionRequiredMixin, DetailView): class TimerDetail(PermissionRequiredMixin, DetailView):
@ -477,55 +477,12 @@ class TimerRestart(PermissionRequiredMixin, RedirectView):
return reverse("core:timer-detail", kwargs={"pk": kwargs["pk"]}) 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): class TimerDelete(CoreDeleteView):
model = models.Timer model = models.Timer
permission_required = ("core.delete_timer",) permission_required = ("core.delete_timer",)
success_url = reverse_lazy("core:timer-list") 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): class TummyTimeList(PermissionRequiredMixin, BabyBuddyFilterView):
model = models.TummyTime model = models.TummyTime
template_name = "core/tummytime_list.html" template_name = "core/tummytime_list.html"

View File

@ -3,16 +3,16 @@
{% block header %} {% block header %}
<a href="{% url "core:timer-list" %}"> <a href="{% url "core:timer-list" %}">
{% trans "Active Timers" %} {% trans "Timers" %}
</a> </a>
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% with instances|length as count %} {% with instances|length as count %}
{% blocktrans trimmed count counter=count %} {% blocktrans trimmed count counter=count %}
{{ count }} active timer {{ count }} timer
{% plural %} {% plural %}
{{ count }} active timers {{ count }} timers
{% endblocktrans %} {% endblocktrans %}
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View File

@ -707,10 +707,10 @@ def card_timer_list(context, child=None):
if child: if child:
# Get active instances for the selected child _or_ None (no child). # Get active instances for the selected child _or_ None (no child).
instances = models.Timer.objects.filter( instances = models.Timer.objects.filter(
Q(active=True), Q(child=child) | Q(child=None) Q(child=child) | Q(child=None)
).order_by("-start") ).order_by("-start")
else: else:
instances = models.Timer.objects.filter(active=True).order_by("-start") instances = models.Timer.objects.order_by("-start")
empty = len(instances) == 0 empty = len(instances) == 0
return { return {

View File

@ -323,7 +323,7 @@ class TemplateTagsTestCase(TestCase):
data = cards.card_timer_list(self.context) data = cards.card_timer_list(self.context)
self.assertIsInstance(data["instances"][0], models.Timer) 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) data = cards.card_timer_list(self.context, child)
self.assertIsInstance(data["instances"][0], models.Timer) self.assertIsInstance(data["instances"][0], models.Timer)

View File

@ -222,9 +222,7 @@ Note the timer `id` in the response:
"child": 1, "child": 1,
"name": null, "name": null,
"start": "2022-05-28T19:59:40.013914Z", "start": "2022-05-28T19:59:40.013914Z",
"end": null,
"duration": null, "duration": null,
"active": true,
"user": 1 "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: Also note that the timer has been deleted.
```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
}
```
### Response ### Response

View File

@ -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 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. - 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. - 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. - 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 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. - Today's Naps lists the number of naps taken that day in bold and the total nap time below.

206
static/babybuddy/js/app.d20500d757a5.js generated Normal file
View File

@ -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);

BIN
static/babybuddy/js/app.d20500d757a5.js.gz generated Normal file

Binary file not shown.

View File

@ -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); 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);

Binary file not shown.

File diff suppressed because one or more lines are too long