Add carousel of last 7 days of feedings to the todays feeding card (#450)

Add list of last 7 days of feedings to the todays feeding card

Co-authored-by: Daniel Beard <daniel@medcrypt.co>
This commit is contained in:
Daniel Beard 2022-05-30 19:43:42 -07:00 committed by GitHub
parent 7178bf9fa4
commit 21b8a737c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 36 deletions

View File

@ -311,8 +311,8 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
{ {
"id": 3, "id": 3,
"child": 1, "child": 1,
"start": "2017-11-18T14:00:00-05:00", "start": "2017-11-18T19:00:00-05:00",
"end": "2017-11-18T14:15:00-05:00", "end": "2017-11-18T19:15:00-05:00",
"duration": "00:15:00", "duration": "00:15:00",
"type": "formula", "type": "formula",
"method": "bottle", "method": "bottle",
@ -333,7 +333,7 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
self.endpoint, {"start_min": "2017-11-18T11:30:00-05:00"} self.endpoint, {"start_min": "2017-11-18T11:30:00-05:00"}
) )
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 2) self.assertEqual(response.data["count"], 3)
def test_post(self): def test_post(self):
data = { data = {

View File

@ -234,8 +234,8 @@
"fields": "fields":
{ {
"child": 1, "child": 1,
"start": "2017-11-18T14:00:00Z", "start": "2017-11-18T14:00:00-05:00",
"end": "2017-11-18T14:30:00Z", "end": "2017-11-18T14:30:00-05:00",
"duration": "00:30:00", "duration": "00:30:00",
"type": "breast milk", "type": "breast milk",
"method": "left breast", "method": "left breast",
@ -248,8 +248,8 @@
"fields": "fields":
{ {
"child": 1, "child": 1,
"start": "2017-11-18T16:30:00Z", "start": "2017-11-18T16:30:00-05:00",
"end": "2017-11-18T17:00:00Z", "end": "2017-11-18T17:00:00-05:00",
"duration": "00:30:00", "duration": "00:30:00",
"type": "breast milk", "type": "breast milk",
"method": "right breast", "method": "right breast",
@ -262,8 +262,8 @@
"fields": "fields":
{ {
"child": 1, "child": 1,
"start": "2017-11-18T19:00:00Z", "start": "2017-11-18T19:00:00-05:00",
"end": "2017-11-18T19:15:00Z", "end": "2017-11-18T19:15:00-05:00",
"duration": "00:15:00", "duration": "00:15:00",
"type": "formula", "type": "formula",
"method": "bottle", "method": "bottle",
@ -271,6 +271,51 @@
"notes": "forgot vitamins :(" "notes": "forgot vitamins :("
} }
}, },
{
"model": "core.feeding",
"pk": 4,
"fields":
{
"child": 1,
"start": "2017-11-17T19:00:00-05:00",
"end": "2017-11-17T19:15:00-05:00",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
"amount": 0.25,
"notes": "forgot vitamins :("
}
},
{
"model": "core.feeding",
"pk": 5,
"fields":
{
"child": 1,
"start": "2017-11-11T19:00:00-05:00",
"end": "2017-11-11T19:15:00-05:00",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
"amount": 10.0,
"notes": "last day feedings"
}
},
{
"model": "core.feeding",
"pk": 6,
"fields":
{
"child": 1,
"start": "2017-11-11T00:00:00-05:00",
"end": "2017-11-11T00:15:00-05:00",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
"amount": 10.0,
"notes": "oldest feeding at midnight"
}
},
{ {
"model": "core.note", "model": "core.note",
"pk": 1, "pk": 1,

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
from django import template from django import template
from django.utils import timesince, timezone from django.utils import timesince, timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -90,3 +92,25 @@ def seconds(duration):
return s return s
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
@register.filter()
def dayssince(value, today=None):
"""
Returns the days since passed datetime in a user friendly way. (e.g. today, yesterday, 2 days ago, ...)
:param value: a date instance
:param today: date to compare to (defaults to today)
:returns: the formatted string
"""
if today is None:
today = timezone.datetime.now().date()
delta = today - value
if delta < datetime.timedelta(days=1):
return "today"
if delta < datetime.timedelta(days=2):
return "yesterday"
# use standard timesince for anything beyond yesterday
return str(delta.days) + " days ago"

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import unittest
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import timezone, formats from django.utils import timezone, formats
@ -62,6 +64,37 @@ class TemplateTagsTestCase(TestCase):
self.assertEqual(duration.seconds(""), 0) self.assertEqual(duration.seconds(""), 0)
self.assertRaises(TypeError, duration.seconds("not a delta")) self.assertRaises(TypeError, duration.seconds("not a delta"))
def test_duration_dayssince(self):
# test with a few different dates that could be pathological
dates = [
timezone.datetime(2022, 1, 1, 0, 0, 1).date(), # new year
timezone.datetime(2021, 12, 31, 23, 59, 59).date(), # almost new year
timezone.datetime(
1969, 2, 1, 23, 59, 59
).date(), # old but middle of the year
]
for d in dates:
self.assertEqual(duration.dayssince(d, today=d), "today")
self.assertEqual(
duration.dayssince((d - timezone.timedelta(hours=5)), today=d), "today"
)
self.assertEqual(
duration.dayssince((d - timezone.timedelta(hours=24)), today=d),
"yesterday",
)
self.assertEqual(
duration.dayssince((d - timezone.timedelta(hours=24 * 2)), today=d),
"2 days ago",
)
self.assertEqual(
duration.dayssince((d - timezone.timedelta(hours=24 * 10)), today=d),
"10 days ago",
)
self.assertEqual(
duration.dayssince((d - timezone.timedelta(hours=24 * 60)), today=d),
"60 days ago",
)
def test_instance_add_url(self): def test_instance_add_url(self):
child = Child.objects.create( child = Child.objects.create(
first_name="Test", last_name="Child", birth_date=timezone.localdate() first_name="Test", last_name="Child", birth_date=timezone.localdate()

View File

@ -8,15 +8,42 @@
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% if total %} {% if feedings|length > 0 %}
{{ total }} <div id="feeding-days-carousel" class="carousel slide" data-interval="false">
{% else %} <div class="carousel-inner">
{% trans "None" %} {% for feeding in feedings %}
<div class="carousel-item{% if forloop.counter == 1 %} active{% endif %}">
<div class="last-feeding-method text-center">
{% if feeding.total %}
{{ feeding.total }}
{% else %}
{% trans "None" %}
{% endif %}
</div>
<div class="text-center small">
{% if feeding.count > 0 %} {{ feeding.count }} feedings {% endif %}
</div>
{% blocktrans trimmed with since=feeding.date.date|dayssince %}
<div class="text-center small text-muted">
{{ since }}
</div>
{% endblocktrans %}
</div>
{% endfor %}
</div>
{% if feedings|length > 1 %}
<a class="carousel-control-prev" href="#feeding-days-carousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">{% trans "Previous" %}</span>
</a>
<a class="carousel-control-next" href="#feeding-days-carousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">{% trans "Next" %}</span>
</a>
{% endif %} {% endif %}
{% endblock %} </div>
{% else %}
{% trans "None" %}
{% endif %}
{% block content %} {% endblock %}
{% if count > 0 %}
{% blocktrans %}{{ count }} feeding entries{% endblocktrans %}
{% endif %}
{% endblock %}

View File

@ -107,31 +107,46 @@ def card_diaperchange_types(context, child, date=None):
@register.inclusion_tag("cards/feeding_day.html", takes_context=True) @register.inclusion_tag("cards/feeding_day.html", takes_context=True)
def card_feeding_day(context, child, date=None): def card_feeding_day(context, child, end_date=None):
""" """
Filters Feeding instances to get total amount for a specific date. Filters Feeding instances to get total amount for a specific date and for 7 days before
:param child: an instance of the Child model. :param child: an instance of the Child model.
:param date: a Date object for the day to filter. :param end_date: a Date object for the day to filter.
:returns: a dict with count and total amount for the Feeding instances. :returns: a dict with count and total amount for the Feeding instances.
""" """
if not date: if not end_date:
date = timezone.localtime().date() end_date = timezone.localtime()
# push end_date to very end of that day
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=9999)
# we need a datetime to use the range helper in the model
start_date = end_date - timezone.timedelta(
days=8
) # end of the -8th day so we get the FULL 7th day
instances = models.Feeding.objects.filter(child=child).filter( instances = models.Feeding.objects.filter(child=child).filter(
start__year=date.year, start__month=date.month, start__day=date.day start__range=[start_date, end_date]
) | models.Feeding.objects.filter(child=child).filter(
end__year=date.year, end__month=date.month, end__day=date.day
) )
total = sum([instance.amount for instance in instances if instance.amount]) # prepare the result list for the last 7 days
count = len(instances) dates = [end_date - timezone.timedelta(days=i) for i in range(8)]
empty = len(instances) == 0 or total == 0 results = [{"date": d, "total": 0, "count": 0} for d in dates]
# do one pass over the data and add it to the appropriate day
for instance in instances:
# convert to local tz and push feed_date to end so we're comparing apples to apples for the date
feed_date = timezone.localtime(instance.start).replace(
hour=23, minute=59, second=59, microsecond=9999
)
idx = (end_date - feed_date).days
result = results[idx]
result["total"] += instance.amount if instance.amount is not None else 0
result["count"] += 1
return { return {
"feedings": results,
"type": "feeding", "type": "feeding",
"total": total, "empty": len(instances) == 0,
"count": count,
"empty": empty,
"hide_empty": _hide_empty(context), "hide_empty": _hide_empty(context),
} }

View File

@ -156,8 +156,17 @@ class TemplateTagsTestCase(TestCase):
self.assertEqual(data["type"], "feeding") self.assertEqual(data["type"], "feeding")
self.assertFalse(data["empty"]) self.assertFalse(data["empty"])
self.assertFalse(data["hide_empty"]) self.assertFalse(data["hide_empty"])
self.assertEqual(data["total"], 2.5) # most recent day
self.assertEqual(data["count"], 3) self.assertEqual(data["feedings"][0]["total"], 2.5)
self.assertEqual(data["feedings"][0]["count"], 3)
# yesterday
self.assertEqual(data["feedings"][1]["total"], 0.25)
self.assertEqual(data["feedings"][1]["count"], 1)
# last day
self.assertEqual(data["feedings"][-1]["total"], 20.0)
self.assertEqual(data["feedings"][-1]["count"], 2)
def test_card_feeding_last(self): def test_card_feeding_last(self):
data = cards.card_feeding_last(self.context, self.child) data = cards.card_feeding_last(self.context, self.child)
@ -246,7 +255,7 @@ class TemplateTagsTestCase(TestCase):
}, },
{ {
"type": "duration", "type": "duration",
"stat": timezone.timedelta(0, 7200), "stat": timezone.timedelta(days=1, seconds=46980),
"title": "Feeding frequency", "title": "Feeding frequency",
}, },
{ {